2 Punkte von GN⁺ 2024-11-03 | 1 Kommentare | Auf WhatsApp teilen
  • Beim Bau eines neuen Syntax-Highlighters für llamafile zeigte sich nach der Beschäftigung mit 42 Sprachen, dass selbst in einfachem Lexing viele sprachspezifische Ausnahmen und alte Syntax verborgen sind
  • Die Implementierung nutzt C++ und GNU gperf, um Keyword-Lookups zu beschleunigen; die auf Strings, Kommentare und Keywords fokussierte Verarbeitung wird mit einer auf for-Schleifen und switch basierenden endlichen Zustandsmaschine gelöst
  • C-Trigraphen, die Zeilentrenner u2028 und u2029 in JavaScript, Heredocs in Shell, Perl und Ruby sowie String-Interpolation in Kotlin, Scala, TypeScript und Swift treten wiederholt als Fälle auf, die sich nur mit Lexing schwer behandeln lassen
  • Gemessen an Codezeilen reicht die Spanne von 125 Zeilen für FORTH bis 1042 Zeilen für Ruby; bei Ruby kollidieren der Operator <<, Heredocs und Backquotes, wodurch Highlighting ohne Parsing besonders schwierig ist
  • Der neue Highlighter von llamafile wurde unter Windows 10 mit Meta LLaMA 3.2 3B Instruct demonstriert und läuft auch unter macOS, Linux, FreeBSD und NetBSD; damit hebt er sich von ollama ab, das kein Syntax-Highlighting bietet

Anlass für den llamafile-Highlighter

  • Um einen neuen Syntax-Highlighter für llamafile zu bauen, wurden einen Monat lang 42 Programmiersprachen untersucht
  • Unterstützt werden Ada, Assembly, BASIC, C, C#, C++, COBOL, CSS, D, FORTH, FORTRAN, Go, Haskell, HTML, Java, JavaScript, Julia, JSON, Kotlin, ld, LISP, Lua, m4, Make, Markdown, MATLAB, Pascal, Perl, PHP, Python, R, Ruby, Rust, Scala, Shell, SQL, Swift, Tcl, TeX, TXT, TypeScript und Zig
  • Diese Liste deckt den Großteil des TIOBE Index ab; Scratch wurde jedoch ausgeschlossen, weil es Blöcke statt Text verwendet und daher nicht zum Highlighting-Ziel gehört

Implementierungsansatz: gperf und endliche Zustandsmaschinen

  • Der größte Engpass bei einem einfachen Syntax-Highlighter entsteht dort, wo sich wiederholte String-Vergleiche häufen, um zu entscheiden, ob ein Token ein Keyword ist
  • Mit C++ und GNU gperf wird eine perfekte Hash-Tabelle erzeugt
    • Eine beispielhafte gperf-Eingabe definiert Java-Konstanten wie true, false und null als Keywords
    • Die von gperf erzeugte C-Datei kann eine Hash-Funktion erstellen, die für kollisionsfreie Lookups nur ein einzelnes Zeichen berücksichtigt
  • C-Highlighting lässt sich dank gperf selbst mit rund 4.000 definierten Keywords mit 35 MB/s verarbeiten
  • Der übrige Teil lässt sich meist gut mit einer endlichen Zustandsmaschine abdecken
    • Ohne flex, bison oder ragel kann man mit nur einer for-Schleife und switch einen einfachen Highlighter bauen
    • Wenn man sich auf Strings, Kommentare und Keywords konzentriert, reicht im Allgemeinen die Lexing-Ebene aus
    • Für Elemente wie C-Funktionsnamen kann echtes Parsing nötig sein
  • highlight_ada.cpp dient als Beispielimplementierung

llamafile-Demo und Einsatzumgebung

  • Der neue Highlighter und das Chatbot-Interface haben die Nutzbarkeit von llamafile verbessert; Syntax-Highlighting, das ollama nicht bietet, ist ein Unterscheidungsmerkmal
  • Die Demo lief unter Windows 10 mit dem Modell Meta LLaMA 3.2 3B Instruct
  • Diese llamafile ist auch unter macOS, Linux, FreeBSD und NetBSD lauffähig
  • Da die Qualität offener Weight-Modelle wie gemma 27b it besser wird, sinkt der Anreiz, Claude zu verwenden

C: Lexing-Ausnahmen entgegen dem Eindruck einer einfachen Sprache

  • Entgegen dem Eindruck, C sei einfach, sind seine lexikalischen Elemente sehr ungewöhnlich
  • Trigraphen können Zeichen wie #, [, \, ^, {, |, }, ~ durch ??=, ??(, ??/, ??), ??', ??<, ??!, ??>, ??- ersetzen
    • Sie wurden aus dem C23-Standard entfernt, doch wegen Legacy-Software dürften Compiler sie weiter unterstützen
    • Ein guter Syntax-Highlighter sollte auch diese Syntax verarbeiten
  • Universal Characters in C können Unicode-Identifier in ASCII-Quellcode darstellen, etwa int \uFEB2 = 1;
    • GCC gibt einen Fehler aus, wenn es sich nicht um bestimmte Unicode-Planes handelt, die das Standardisierungskomitee erlaubt hat
  • Einzeilige Kommentare in C können sich über mehrere Zeilen erstrecken, wenn am Zeilenende ein Backslash steht
    • Sprachen wie Perl, Ruby und Shell, die Backslash-Escapes im Quellcode erlauben, unterstützen dieses C-artige Verhalten nicht
    • Tcl und GNU Make unterstützen dieses Verhalten offenbar
    • Emacs und Pygments behandeln dies in manchen Fällen falsch, während Vim den Backslash offenbar immer korrekt verarbeitet
  • C kennt auch eine Null-Preprocessor-Directive
    • .c-Dateien aus frühen v6-Quellcodes beginnen oft mit einer Zeile, die nur # enthält
    • Das ist auch heute gültiger Code und kann verwendet werden, um den Präprozessor trotz angeforderter Kommentarerhaltung mit cc -C -E bestimmte Kommentare entfernen zu lassen

Kommentar-Syntax: Haskell und D

  • In C können mehrzeilige Kommentare nicht erneut mehrzeilige Kommentare enthalten
  • Haskell unterstützt verschachtelte Kommentare der Form {- ... {- ... -} ... -}
  • D übernimmt C-Kommentare mit // und /* ... */ unverändert und führt zusätzlich eine rekursive Kommentar-Syntax der Form /+ ... +/ ein
  • Die Dokumentation der lexikalischen Syntax von D ist formal und detailliert und liefert die für die Implementierung nötigen Informationen gut

Tcl und JavaScript: Anführungszeichen und unsichtbare Zeilentrenner

  • In Tcl können Anführungszeichen in Identifiern vorkommen
    • puts a"b gibt a"b aus
    • Auch Variablennamen können Anführungszeichen enthalten; beim Referenzieren muss jedoch die Schreibweise ${a"b} statt $a"b verwendet werden
  • JavaScript hat eine eingebaute lexikalische Syntax für reguläre Ausdrücke
    • In /[/]/g muss / innerhalb der eckigen Klammern einer Zeichenklasse nicht escaped werden
    • Eine Methode, die nur den schließenden Slash scannt, kann bei minifiziertem Code falsch liegen
  • ECMAScript definiert u2028 LINE SEPARATOR und u2029 PARAGRAPH SEPARATOR als Zeilentrenner
    • Diese Zeichen verhalten sich im Grunde wie \n
    • Da es Trojan-Source-Zeichen sind, werden sie in Emacs so konfiguriert, dass sie als bzw. sichtbar sind
    • Viele Software erkennt diese Zeichen nicht und rendert sie teils als Fragezeichen
    • Abgesehen von D sei keine andere Sprache bekannt, die so damit umgeht
  • Diese Eigenschaft ermöglichte in SectorLISP ein Polyglot aus C und JavaScript
    • lisp.js läuft im Browser und kann mit GCC kompiliert auch lokal ausgeführt werden
    • llamafile hebt solchen Code korrekt hervor; bei anderen Highlightern sei keine entsprechende Behandlung gefunden worden

Randfälle bei Shell und Heredocs

  • Ein Heredoc in Shell schreibt mehrzeilige Strings in der Form cat <<EOF ... EOF
  • Die quoted-Heredoc-Syntax cat <<'END' deaktiviert Variablensubstitution
  • Wenn der Heredoc-Marker als leerer String gesetzt wird, endet das Heredoc bei der nächsten leeren Zeile
    • Das Beispielprogramm gibt hello und world in zwei Zeilen aus
  • In Sprachen mit Heredoc-Unterstützung wie Shell, Ruby und Perl können mehrere Heredocs in derselben Zeile stehen
  • Wie in Tcl beginnt # in Shell nicht immer einen Kommentar
    • In ${x#hi-} wird # zum Entfernen eines Präfixes verwendet; das Beispiel gibt there aus

String-Interpolation: Sprachen, die einen Zustands-Stack brauchen

  • Selbst wenn ein Kotlin-String mit " beginnt, kann er wegen String-Interpolation bei { in einen anderen Zustand wechseln
  • TypeScript, Swift, Kotlin und Scala unterstützen String-Interpolation stark in Richtung echten Codes innerhalb von Strings
  • Um Kotlin, Scala und TypeScript hervorzuheben, muss man geschweifte Klammern zählen und einen Parser-Zustands-Stack pflegen
    • TypeScript ist vergleichsweise einfach; einige zusätzliche Zustände in der endlichen Zustandsmaschine reichen aus
    • Kotlin und Scala unterstützen Interpolation sowohl in Double-Quote- als auch in Triple-Quote-Syntax, sodass allein für String-Lexing rund 13 unabhängige Zustände nötig waren
    • Swift unterstützt die Interpolationssyntax "\(var)" und Triple Quotes, benötigte in der Implementierung aber 10 Zustände

Swift, C# und FORTH: Unterschiede beim Einfassen von Strings

  • Swift unterstützt Syntax, bei der "...", """...""" und /regex/-Strings mit einer beliebigen Anzahl von # eingefasst werden
    • Die Anzahl der # auf beiden Seiten muss übereinstimmen
    • Dieser Ansatz löst das Problem, Anführungszeichen oder Regex-Grenzen in Strings unterzubringen
  • C# ähnelt den mehrzeiligen Triple-Quote-Strings von Python, kann aber am Anfang und Ende eine höhere Anzahl übereinstimmender Anführungszeichen verwenden
    • Die Zahl der Anführungszeichen links wird zur Abschlussbedingung rechts
    • Beliebig viele Anführungszeichen zuzulassen, verringert im Vergleich zu klassischen Python-Triple-Quotes die Gültigkeitsregeln und lässt sich mit einer endlichen Zustandsmaschine einfacher dekodieren
  • FORTH tokenisiert alles anhand von Whitespace-Grenzen
    • Auch die String-Startsyntax c" ist ein eigenes Token
    • c" hello world" bedeutet dasselbe wie "hello world" in anderen Sprachen

FORTRAN und COBOL: Regeln fester Spalten

  • llamafile wird als möglicher Use Case dafür genannt, bei der Wartung von Banksystemen zu helfen, auch nachdem FORTRAN- und COBOL-Programmierer in Rente gegangen sind
  • In Air-Gap-Umgebungen könne man eine kontrollierbare KI wie Gemma 27b nach COBOL- und FORTRAN-Code fragen
  • Die Regeln fester Spalten in FORTRAN lauten:
    • Steht in Spalte 1 *, c oder C, ist diese Zeile ein Kommentar
    • Steht in Spalte 6 ein Nicht-Leerzeichen, kann eine Zeile fortgesetzt werden, die länger als 80 Zeichen ist
    • Zahlen in den Spalten 1 bis 5 werden zu Labels
  • Die COBOL-Regeln lauten:
    • Steht in Spalte 7 *, ist es ein Kommentar
    • Steht in Spalte 7 -, kann eine Zeile fortgesetzt werden, die länger als 80 Zeichen ist
    • In den Spalten 1 bis 6 steht die Zeilennummer

Zig und Lua: unterschiedliche Lösungen für mehrzeilige Strings

  • Zig hat eine Syntax für mehrzeilige Strings, die mit zwei Backslashes beginnt
    • Diese Syntax macht den Aufruf von textwrap.dedent() bei Python-Triple-Quote-Strings überflüssig
    • Als Nachteil wird gesehen, dass Semikolons nicht schön aussehen
    • Sie wird als String-Syntax vorgeschlagen, die Sprachen ohne Semikolonpflicht wie Go, Scala und Python in Betracht ziehen könnten
  • Mehrzeilige Strings in Lua basieren auf [[...]]; zwischen die eckigen Klammern kann eine beliebige Anzahl von = gesetzt werden
    • Anfang und Ende müssen wie in [==[ ... ]==] dieselbe Anzahl von = haben
    • Dasselbe Verfahren kann auch für Kommentare genutzt werden
    • Sowohl --[[ ... ]] als auch --[==[ ... ]==] sind möglich

Assembly: Kombination aus Dialekten und Präprozessoren

  • Assembly ist wegen seiner vielen Dialekte eine der schwer zu highlightenden Sprachen
  • llamafile zielt darauf ab, mehrere Syntaxvarianten wie AT&T und nasm vernünftig zu verarbeiten
  • Wenn Keywords als erster Identifier in einer Zeile behandelt werden, sofern sie nicht nach einem Doppelpunkt stehen, lässt sich der meiste Assembly-Code ordentlich darstellen
  • Auch die Kommentar-Syntax ist nicht einfach
    • Ursprüngliche UNIX-Kommentare brauchten nur ein einzelnes /
    • GNU as unterstützt diesen Kommentar noch, aber nur am Zeilenanfang
    • Da Clang fixed comments nicht unterstützt, sei dies in Open-Source-Code praktisch schwer nutzbar
  • Der ursprüngliche UNIX-Assembler verwendete bei Zeichenliteralen kein schließendes Anführungszeichen
    • 'x steht für den Wert 0x78 des Zeichens x
    • GNU as unterstützt dies weiterhin, LLVM jedoch nicht
    • Da diese Syntax in bestehendem Code vorkommt, sollte ein guter Highlighter sie unterstützen
  • GNU assembler erlaubt quotierte Identifier, sodass Symbole nahezu beliebige Zeichen enthalten können
  • Assembly wird häufig zusammen mit dem C-Präprozessor oder m4 verwendet
    • Auch Zeilen, die mit dnl, m4_dnl oder C beginnen, müssen als Kommentare behandelt werden

Ada und BASIC: kleine Syntaxdetails, die Lexing erschüttern

  • Ada ist recht einfach zu lexen, verwendet einfache Anführungszeichen aber auf ungewöhnliche Weise
    • Wie C kann es Zeichenliterale der Form 'x' haben
    • Auch beim Referenzieren von Attributen wie Foo'Size werden einfache Anführungszeichen verwendet
    • Character'(')')'Image deklariert ein Zeichen und wandelt es über die Funktion Image in eine String-Repräsentation um
  • Commodore-BASIC-Beispiele brechen mehrere Annahmen über Syntax-Highlighting
    • Bei Strings kann das schließende Anführungszeichen am Zeilenende weggelassen werden
    • Variablennamen tragen Sigils wie $
    • Keywords wie goto werden auch innerhalb von Identifiern aktiv lexikalisch erkannt
  • Visual BASIC hat eine Syntax für Datums-Literale wie #1/1/2024#
  • Visual BASIC besitzt außerdem Präprozessor-Direktiven wie #If DEBUG Then, #Else und #End If, was das Lexing schwierig macht

Perl: Komplexität zwischen Shell und Programmiersprache

  • Perl steht zwischen Shell und Programmiersprache und erbt die Komplexität beider Seiten
  • Es machte reguläre Ausdrücke zu einem First-Class-Element der Sprache, was auch andere Sprachen wie Python beeinflusste
  • Perls Substitutionssyntax ähnelt sed, etwa s/hello/Perl/i
    • Statt / kann jedes Satzzeichen als Trennzeichen verwendet werden
    • Das ist praktisch, wenn Slashes im regulären Ausdruck vorkommen, etwa s!hello!Perl!i
    • Bei gepaarten Zeichen wie s{hello}{Perl}i ist ein zusätzliches Zeichen nötig
  • Perl hat viele magische Präfixe, die wie Strings hervorgehoben werden müssen
    • m, s, y, qr, qw, qq, qx usw. werden mit verschiedenen Trennzeichen verwendet
  • Um y/x/y/ nicht fälschlich als Division zu interpretieren, muss der Kontext betrachtet werden
    • Da Perl-Variablen Sigils wie $ für Skalare, @ für Arrays und % für Hashes haben, hilft das bei der Unterscheidung, ohne die gesamte Syntax parsen zu müssen
  • In Perl ist es üblich, POD-Dokumentation für man pages in den Quellcode einzubetten
    • =word am Zeilenanfang startet eine POD-Dokumentation, =cut beendet sie

Ruby: das schwierigste Lexing-Ziel

  • Ruby wirkt wie die Vereinigungsmenge der vorherigen Sprachen; die Syntax sei formal nicht ausreichend dokumentiert
  • Die Syntax-Dokumentation im Ruby-Handbuch wird als eher detailarm eingeschätzt
  • Ruby unterstützt Backquote-Syntax und erlaubt zugleich, Backquote wie einen Methodennamen zu verwenden, sodass ein Highlighter nur schwer entscheiden kann, ob es ein String ist oder nicht
  • Ruby kennt sowohl den Operator << als auch Heredocs
    • In echtem Code kommen Formen wie options[:includes] <<arg; true vor
    • Dieser Code kann wie ein Heredoc aussehen; auch Emacs behandelt ihn falsch
  • Auch Code, bei dem ein Heredoc innerhalb von String-Interpolation steht, ist gültig, etwa puts "This is #{<<HERE.strip} evil"
  • Unter den 42 Sprachen galt Ruby als größter Schock; Ruby lasse sich möglicherweise nicht ohne Parsing lexen, und selbst mit Parsing sei schwer sicher zu sein, ob sich der Code interpretieren lässt

Komplexität gemessen an Codezeilen der Implementierung

  • Gemessen an der Zahl der Codezeilen je Sprach-Highlighter ist FORTH am einfachsten und Ruby am komplexesten
  • Die kürzeste Implementierung ist highlight_forth.cpp mit 125 Zeilen
  • Zu den relativ kurzen Implementierungen gehören m4 mit 132 Zeilen, Ada mit 149 Zeilen, LISP mit 160 Zeilen, MATLAB mit 166 Zeilen, COBOL mit 186 Zeilen, BASIC mit 199 Zeilen und FORTRAN mit 200 Zeilen
  • Mittelgroße Implementierungen umfassen JavaScript mit 337 Zeilen, TypeScript mit 371 Zeilen, Kotlin mit 387 Zeilen, Scala mit 387 Zeilen, Assembly mit 447 Zeilen, C mit 449 Zeilen, Swift mit 455 Zeilen und D mit 521 Zeilen
  • Die längsten Implementierungen sind Shell mit 570 Zeilen, Perl mit 583 Zeilen und highlight_ruby.cpp mit 1042 Zeilen

1 Kommentare

 
GN⁺ 2024-11-03
Hacker-News-Kommentare
  • Mein liebster C-Trigraph ist etwas wie do_action() ??!??! handle_error()
    Es sieht wie eine spezielle Fehlerbehandlungssyntax aus, aber tatsächlich wird ??!??! zu einem logischen OR || umgewandelt, und dank der Kurzschlussauswertung wird handle_error() ausgeführt, wenn do_action() einen von 0 verschiedenen Wert zurückgibt, was irgendwie befriedigend ist

  • Ich habe es mit Vergnügen gelesen, fühlte mich aber eher noch stärker zur Lisp-Perspektive hingezogen
    So wie ich es verstanden habe, ist die Syntax in einer Sprache kein besonders wichtiger Teil, neigt eher dazu, ein Hindernis als eine Hilfe zu sein, und sollte daher so einfach und einheitlich wie möglich sein, damit man sich auf anderes konzentrieren kann
    Allerdings war das Erlernen von strukturellem Editieren in Lisp für mich bisher eher ein Hindernis als eine Hilfe, auch wenn ich hoffe, dass es sich irgendwann auszahlt

    • Eine einfache Syntax mag für Computer in Ordnung sein, aber Syntax wird in erster Linie dafür entworfen, dass Menschen sie lesen und schreiben
      Wenn man sie wie in Lisp vereinfacht, wird die Syntaxdiskussion nur auf die Semantik verschoben; im Grunde ändert sich nur die Ebene
      Ich halte eine komplexe Syntax für viel leichter les- und schreibbar als eine einfache Syntax mit komplexer Semantik. Bei Syntaxfehlern bekommt man schnelles Feedback, Semantikfehler können dagegen bis zur Laufzeit verborgen bleiben
    • Lisp hat Reader-Makros, mit denen man den Lexer umprogrammieren kann, und auch der Prozess, sichtbare Struktur in einen Syntaxbaum zu überführen, kann mit Makros behandelt werden
      So etwas wie https://pyret.org/ ist zum Beispiel möglich. Ich würde daher nicht sagen, dass Lisp tatsächlich einfach oder notwendigerweise einheitlich ist
    • Es überrascht mich etwas, dass strukturelles Editieren ein Hindernis war; als Ratschlag würde ich sagen: Nutze parinfer und merke dir nur die Tastenkürzel für die drei Befehle slurp, barf und raise
      Schon mit diesen vier Dingen bekommt man ohne viel Komplexität etwa 95 % der Vorteile von paredit, und den Rest der Tricks kann man lernen, wenn man sich erst einmal daran gewöhnt hat
  • Es gab die Passage „Ich weiß nicht, wer C mit 35 MB/s syntaxhervorheben möchte, aber jetzt ist es möglich“, und ja, das ist schnell, aber tcc kompiliert C mit 29 MB/s in Binärcode, und zwar sogar auf sehr alten Rechnern: https://bellard.org/tcc/#speed
    Es mag noch schneller gehen, aber wahrscheinlich braucht man das nicht

  • Der Autor hat ein paar Dinge übersehen
    Nicht nur TypeScript, Swift, Kotlin und Scala gehen bei String-Interpolation so weit, echten Code einzubetten; auch C#, Python, JavaScript, Ruby, Shell und Make unterstützen Ähnliches
    Bei Tcl ist die Trennung zwischen Code und Daten so unscharf, dass { } effektiv ein ungewöhnlicher String-Begrenzer ist, und bei xyzzy {#hello world} weiß man vor der Auswertung nicht, ob #hello world ein Kommentar oder ein String ist
    PostgreSQL hat praktische Dollar-Quoted-Strings, sodass wie hier https://www.postgresql.org/docs/current/sql-syntax-lexical.h... 'Dianne''s horse', $$Dianne's horse$$ und $SomeTag$Dianne's horse$SomeTag$ dasselbe bedeuten

    • Perl kann das auch
      Ein String wie "I have $foo $bar's: @{[$bar x $foo]}" kann I have 5 x's: xxxxx ausgeben
      Die Syntax @{[...]} nutzt die Perl-Eigenschaft, dass nicht nur Array-Interpolation, sondern auch Skalare möglich sind; das innere [...] erzeugt eine Array-Referenz, und das äußere @{...} dereferenziert sie. Der Perl-Interpreter erlaubt in diesem inneren Ausdruck beliebigen Code
    • Statt zu sagen, dass „Code in einem String enthalten ist“, ist es meiner Meinung nach treffender, es als besondere Syntax für String-Verkettung zu sehen
      Dann kann man so etwas wie "foo { toUpper("bar { x + y } bar") } foo" verschachteln, und wenn + String-Verkettung ist, ist es im Wesentlichen dasselbe wie "foo " + toUpper("bar " + (x + y) + " bar") + " foo"
      Ich weiß allerdings nicht, ob es eine Sprache gibt, die tatsächlich so funktioniert
    • Ruby treibt das auf die Spitze
      Ich mag Ruby, aber gültiges Ruby wie puts "This is #{< zu verteidigen, das String-Interpolation mit Heredoc mischt, ist schwer
      Außerdem kann in Ruby sogar Leerraum ein Anführungszeichen sein. In einem Kontext ohne linken Operanden beginnt % einen zitierten String, und das nächste Zeichen bestimmt die Art des Quotes, daher wird % hello zu einem String mit hello. %(this is a string) oder %{this is a string} sind in Ordnung, aber Leerraum dafür zu verwenden habe ich in freier Wildbahn nie gesehen, und selbst irb verarbeitet es nicht richtig, also dürfte es ruhig entfernt werden
    • Scalas String-Interpolation kann auch als Muster für Pattern Matching verwendet werden
      Wenn man val s"${a} + ${b}" = "1 + 2" schreibt, wird a zu 1 und b zu 2
    • Das Coole an interpolierten Strings in C# ist, dass sie lazy ausgewertet werden
      Früher rief Code wie log.trace($"Entering iteration {i} for customer {c.ID} [{c.ShortName}]"); in heißen Loops jedes Mal string.Concat auf, noch bevor ein Logger überhaupt frühzeitig zurückkehrte, weshalb viele Logger ihre eigene Interpolation implementierten
      In C# kann man Overloads deklarieren, die DefaultInterpolatedStringHandler oder das Muster eines Custom Handlers akzeptieren; dieser Overload wird bevorzugt, sodass man erst prüfen kann, ob das Logging tatsächlich nötig ist, und die String-Erzeugung dann verzögert
  • Es gibt noch eine weitere grammatische Eigenheit, die hier nicht erwähnt wurde und die die meisten Syntax-Highlighter kaputtmacht
    In Java können Unicode-Escapes nicht nur in Strings, sondern überall vorkommen
    Zum Beispiel ist class Foo\u007b} eine gültige Klasse, und \u000A innerhalb eines //-Kommentars kann als echter Zeilenumbruch behandelt werden, sodass auch das assert-Beispiel anders funktioniert als erwartet

    • Dass man das nicht korrekt syntax-highlighten kann, kann man auch als Sicherheitsproblem ansehen
      Da sich mit einem Unicode-Escape ein Blockkommentar beenden lässt, muss man, um bösartigen Code in Kommentaren einer Java-Quelldatei zu verstecken, nur irgendeinen Vorwand schaffen, warum sich im Kommentar ein Block aus Unicode-Escapes befinden könnte
      Entwickler, die diese Eigenschaft nicht kennen, werden es leicht für auskommentiert halten und einfach darüber hinweggehen
    • So etwas habe ich in Java noch nie gesehen, aber gibt es dafür tatsächlich einen nützlichen Anwendungsfall?
  • Es gibt die Stelle „Ruby ist eine Sprache, die allen Versuchen entgeht, ihre Grammatik zu verstehen“, aber aus Sicht von TeX mit seinem beliebig umprogrammierbaren Lexer ist das noch ziemlich niedlich

    • Auch Lisp-Reader-Makros können den Lexer programmieren
  • „Jeder C-Programmierer weiß, dass man keine mehrzeiligen Kommentare in mehrzeilige Kommentare setzen kann“, aber für Standard-ML-Programmierer kann das wie eine überraschende Einschränkung wirken
    Verschachtelte Kommentare wie (* (* Nested (**) *) comment *) sind gültig, und das folgende val _ = print "hello, world\n" wird ebenfalls normal ausgeführt
    Wenn man bedenkt, dass C bei seinem Erscheinen als ausdrucksstarke Sprache galt, ist es interessant, dass verschachtelte Kommentare nicht in die Sprache aufgenommen wurden

    • Daran sind drei Dinge interessant
      ML hatte keine einzeiligen Kommentare, also ist das eine ähnlich überraschende Einschränkung, und C als „ausdrucksstark“ zu bezeichnen, höre ich zum ersten Mal, aber 1972 mag das im Vergleich zu Assembler so gewirkt haben
      Und ich weiß nicht, was die Kommentarsyntax mit der Ausdrucksstärke einer Sprache zu tun haben soll; per Definition würde ich sagen: überhaupt nichts
    • Um verschachtelte Kommentare zu lexen, braucht man einen Stack oder zumindest einen Zähler für die Verschachtelungstiefe
      Traditionell wurde lexikalische Analyse als Bereich betrachtet, der nur endliche Zustandsautomaten wie bei regulären Ausdrücken verwendet, daher wirkten solche Funktionen als etwas außerhalb des Scopes des Lexings
    • Auch in C gibt es mit #if 0 eine Möglichkeit, Kommentare zu verschachteln
      Man kann also etwas wie #if 0 ... #if 0 ... #endif ... #endif darum herumlegen
    • Pascal unterstützte ebenfalls schon immer eine verschachtelte Kommentarsyntax wie im Beispiel
    • Das gilt nicht nur für Standard ML, sondern für ML allgemein
  • Ich würde gern die Syntax-Highlighting-Implementierung von joe in wiederverwendbarer Form sehen
    Das Format von https://joe-editor.sf.net/ ist mächtig genug, um Python-f-Strings korrekt hervorzuheben
    Zugehörige Dokumentation und Beispiele gibt es unter https://github.com/cmur2/joe-syntax/blob/joe-4.4/misc/HowItW... und https://gist.github.com/irdc/6188f11b1e699d615ce2520f03f1d0d...

    • Auf Basis von joes DFA-basierter Parsing-Methode habe ich mehrere Lexer und Parser gebaut
      Die Zustands- und Übergangsgrammatik war viel leichter zu verstehen als bei den Standardwerkzeugen
      Der Nachteil ist, dass die Regelsätze langatmig werden und eine ideale Strukturierung etwas schwieriger ist, aber ich sehe das sogar als Vorteil, weil sich dadurch alle Erzeugungsregeln im Code leichter nachvollziehen lassen
    • Interessanterweise hat sich die Syntax von Python-f-Strings in 3.12 geändert, daher müsste sich auch das Highlighting je nach Version unterscheiden
  • Der Autor scheint nie versucht zu haben, TeX-Syntax hervorzuheben
    Das ist vielleicht gut für die psychische Gesundheit, denn bei TeX ist vollständiges Syntax-Highlighting in der Regel ohne Interpretation unmöglich
    Parsen allein reicht nicht einmal aus, weil man die Funktion jedes Zeichens neu definieren kann, also Dinge wie „Ab jetzt ist K { und C ist }“ möglich sind
    Tatsächlich gibt es sogar arXiv-Paper, die diese verfluchte Funktion verwenden

    • Ich habe https://github.com/Mozilla-Ocho/llamafile/blob/main/llamafil... erstellt, und für die .tex-Dateien, die ich auf meiner Festplatte gefunden habe, highlightet es ganz ordentlich, ohne kaputtzugehen
      Das Ziel ist, 99,9 % des realen Einsatzes abzudecken. Dann deckt es wahrscheinlich auch das meiste ab, was ein LLM ausgeben würde
      Auch komplizierte Grammatik ist meist kein großes Problem, solange Strings oder Kommentare nicht bis zum Dateiende weiterlaufen und den restlichen Quelltext überdecken
    • Ich fand es schwer zu glauben, als ich erfuhr, dass \makeatletter nicht bedeutet, „irgendetwas zu einem at-Zeichen zu machen“, sondern dass beim Parsen das Zeichen @ als Buchstabe behandelt wird
    • Auch Common Lisp kann auf ähnliche Weise seine Readtables neu definieren. Allerdings wird das auf arXiv vermutlich seltener missbraucht
  • Ich glaube nicht, dass es einfach ist, eine gute Syntax-Coloring-Engine wie in Vim zu schreiben.
    Für Syntax-Coloring braucht man Kontextverarbeitung, bei der auf unterschiedlich verschachtelte Inhalte verschiedene Regeln angewendet werden.
    Der Syntax-Highlighter von Vim lässt einen zwei Arten von Einträgen deklarieren, match und region; match ist eine einfache lexikalische Regel, während region Ausdrücke besitzt, die jeweils Anfang, Ende und Mitte abgleichen.
    Einträge können so deklariert werden, dass sie nur aktiv sind, wenn sie sich innerhalb eines bestimmten Bereichs befinden, und auch Einschlussbeziehungen lassen sich gegenseitig deklarieren.
    Auf dieser grundlegenden semantischen Struktur bauen dann noch Funktionen für diverse Sonderfälle auf. Selbst für Justine wäre es wohl schwer, das direkt in einem Vorstellungsgespräch zu bauen; eher etwas für eine Overnight-Aufgabe.

    • Ein schwer zu handhabendes Beispiel ist in TXR eingebettetes TXR Lisp.
      Hier gibt es das genman-Skript, das das Ergebnis einer Manpage-HTML-Konvertierung entgegennimmt und zum HTML des TXR-Handbuchs weiterverarbeitet: https://www.kylheku.com/cgit/txr/tree/genman.txr
      Die weiß erscheinenden Teile sind Literal-Templates, und Lisp-Code steckt in Direktiven wie @(do ...). TXR-Schlüsselwörter erscheinen violett, TXR-Lisp-Schlüsselwörter grün; selbst dasselbe Wort sieht je nach Kontext unterschiedlich aus.
      Quasi-Strings können ebenfalls verschachtelte Grammatik enthalten, und darin können wiederum eingebetteter Code oder weitere Quasi-Strings vorkommen. Die Syntax-Definitionsdateien txr.vim und tl.vim werden beide aus https://www.kylheku.com/cgit/txr/tree/genvim.txr erzeugt.
    • Naiv hätte ich gedacht, dass man für einen wirklich sauberen Syntax-Highlighter zuerst in einen AST parst und dann die Tokens durchläuft, um die Farben je nach Knotentyp zu aktualisieren.
      Wenn man das nicht so macht, dann vermutlich aus Effizienzgründen. Man müsste dann zum Beispiel die gesamte Datei parsen, statt nur den gerade sichtbaren Bereich hervorzuheben, was teuer werden kann.
    • Ich würde allerdings kein Geld darauf wetten, dass Justine das in einem Bewerbungsgespräch nicht hinbekommt ;)