Implementierung: Format

Diese Seite erläutert die Implementierung vom Verarbeiten und Generieren von Ausdrücken in verschiedenen Formaten in der Textengine v0.3. Beschrieben wird der Aufbau der betroffenen Module und ihre Schnittstellen für die Nutzung in anderem C-Code.

Eins der Schwerpunkte der Textengine bilden das Parsen und das Formatieren, d.h. das Einlesen bzw. Erzeugen von Zeichenfolgen. Diese Funktionen sind nicht monolitisch implementiert, sondern auf Basis der sogenannten Format-Manager verteilt. Jeder Format-Manager unterstützt eine bestimmte formale Sprache, Notation oder Darstellungsform und bietet dafür Parser und Formatierer. In der Version 0.3 der Textengine wird die Symbolsprache implementiert. Sie besteht aus dem Symbolformat-Manager für die reine Symbolsprache und dem Stringformat-Manager für in Symbolsprache eingebettete Zeichenketten. Zusätzlich wird ein Integerformat-Manager für die übliche Darstellung von Ganzzahlen bereitgestellt.

Format-Manager

Im C-Code repräsentiert der Typ formatm_t einen Format-Manager, der im Modul format.c implementiert ist. Es handelt sich um einen abstrakten Typ, dessen instanzen jeweils einen abgeleiteten Typ haben: Symbol- (symbolf.c), String- (stringf.c) oder Integerformat (integerf.c). Die Hauptfunktionen des abstrakten Typs sind te_parse und te_format, die das Parsen bzw. Formatieren im jeweiligen Format durchführen.

Zeichenfolgen: Agenten und Quellen

Der Modularität halber bedienen sich die Format-Manager von Vermittlern für die Weitergabe von Zeichenfolgen. Das Modul format.c definiert einen abstrakten Typ für die Quellen (strsrc_t) und einen für die Agenten (strag_t) von Zeichenfolgen. Eine Quelle liefert über die Funktion give_char jeweils ein Zeichen (wchar_t). Ein Agent konsumiert mittels der Funktion take_char jeweils ein Zeichen. Jeder Parser verarbeitet die Zeichenfolge, die aus einer Zeichenquelle stammt, und jeder Formatierer gibt die generierte Zeichenfolge einem Zeichenagenten weiter. Auf dieser Weise kann ein und dieselbe Implementierung von einem Parser Zeichenfolgen verarbeiten, die etwa aus Dateien oder aus C-Strings stammen. Später können neue Quellen hinzugefügt werden (zum Beispiel auslesen vom Textkorpus), die auf Anhieb für alle Parser gelten. Dasselbe gilt für Formatierer, die Zeichenfolgen unterschiedlich ausgeben können. Das Hilfsmodul futils.c (für format utils) bietet momentan Quellen und Agenten für Stream (FILE *), Datei (char *filename) und C-String (char *).

Konfiguration

Die Format-Manager können über eine Konfiguration verfügen. Diese besteht jeweils in einem eigenen Typ, genannt symcf_t fürs Symbol-, strcf_t fürs String- und intcf_t fürs Integerformat. Jeder Typ ist eine Sammlung von Darstellungsoptionen. Jede Instanz des Format-Managers wird mit einer Konfiguration versehen und danach richten sich alle von dieser Instanz bereitgestellten Parser und Formatierer.

Für jeden Konfigurationstyp gibt es im Textkorpus eine passende Texteinheit, die dieselben Angaben enthält. Jeder konfigurierbare Format-Manager bietet eine Funktion te_store_<typ>, die eine Textenheit ausgehend von einer programmatischen Instanz generiert, und eine Funktion te_retrieve_<typ>, die die Angaben einer Texteinheit in einen C-Datentyp überträgt. [Dieses ist noch nicht für alle Format-Manager implementiert.]

Parser

Der abstrakte Typ parser_t, der im Modul format.c definiert wird, repräsentiert einen Parser. Jeder Format-Manager implementiert eine abgeleitete Instanz diesen Typs, die für das Parsen in diesem Format der Konfiguration entsprechend zuständig ist. Unter „Parsen” versteht man hier generell die Verarbeitung von einer Zeichenfolge, die in einem bestimmten Format vorliegt. Einige Parser erfassen direkt Texteinheiten, d. h. tragen sie in das Textkorpus ein. Andere Parser produzieren ein Zwischenergebnis, das sie weitergeben (s.u. die einzelnen Formate). Ein Parser bietet neben der Hauptfunktion parse, die bei jedem Aufruf ein Zeichen verarbeitet, andere wenige Funktionen für die Interaktion mit anderen Parsern und dem System. Ein Parser wird vom Nutzercode nicht direkt instanziiert, sondern indirekt über den Format-Manager und seine Funktion get_parser.

Formatierer

Symmetrisch zum Parsen definiert das Modul format.c den abstrakten Typ formatter_t fürs Formatieren. Unter „Formatieren” versteht man hier das Darstellen einer Texteinheit als Zeichenfolge in einem bestimmten Format mit bestimmter Konfiguration. Einen Formatierer erhält der Nutzercode über die Funktion get_formatter des Format-Managers. Beim Aufruf dieser Funktion übergibt man einen Stringagenten. Der Formatierer gibt dem Stringagenten die generierte Zeichenfolge weiter.

Registrierung

Das Modul format.c unterstützt auch die Registrierung von Formatmanagern und bietet die Funktion te_getformatm, die für eine zu parsende oder zu formatierende Einheit einen passenden Formatmanager mit der impliziten Konfiguration ergibt. Der Formatmanager wird durch das jeweilige Modul einmalig programmatisch registriert (über die Funktionen register_integerf, register_stringf, etc.). Eine Konfiguration wird als implizit registriert, indem sie mit einer untergeordneten Einheit mit der Rolle ~implicit versehen wird. Die Registrierung kann auf einen Datensubtyp durch einen ~represents-Bezug eingeschränkt werden. Eine solche Regsitrierung gilt für jeden Konfigurationstyp bis zum nächsten Überschreiben und kann ins Textkorpus entweder in Symbolsprache erfasst oder programmatisch durch den Aufruf von te_register_formatcf eingetragen werden.

Zeichenkodierung

Parsen und Formatieren sind auf eine einzige Zeichenkodierung ausgelegt (durch den Einsatz eines Unicode-Zeichensatzes stellt dies keine Einschränkung dar). Die Zeichenkodierung kann zu Kompilierzeit festgelegt oder zu Laufzeit aus der Betriebssystemungebung übernommen werden. Dies bestimmt man über den Wert für SYSTEM_LOCALE in config.h.

Die interne Verarbeitung der Zeichen erfolgt grundsätzlich als wchar_t. Da wo ein Multibyte-String konsumiert oder generiert wird, werden die POSIX-Funktionen wctomb, mbtowc etc. eingesetzt.

Symbolformat

Der Symbolformat-Manager, implementiert in symbolf.c, ist für den Umgang mit symbolsprachlichen Ausdrücken zuständig. Er ist allein für die Syntax der Symbolsprache zuständig und überlässt die Semantik einem sogenannten Tokenmanager. Der Symbolparser erfasst aus der Zeichenfolge eine Sequenz von Tokens und übergibt diese dem Tokenagenten. Der Symbolformatierer erhält Tokens von der Tokenquelle und generiert daraus eine Zeichenfolge.

Bei der Initialisierung eines Symbolformat-Managers mit set_symcf legt man eine Konfiguration und einen Tokenmanager fest. Siehe Spec: Symbolsprache für eine kurze Schilderung der Konfigurationsoptionen und die eigentlichen Definitionen in symbolf.h.

Alternativ kann man für die Initialisierung set_symcf_storage benutzen und allein die Konfiguration übergeben. Als Tokenmanager wird in diesem Fall eins eingesetzt, der beim Parsen die Tokens in das Textkorpus speichert und beim Formatieren sie aus diesem holt.

Bemerkung: Bei diesen „Tokens” handelt es sich nicht um die Tokens der Symbolsprache, sondern um die des Textenginekerns. Der Symbolformat-Parser erfasst nicht nur die Tokens der Symbolsprache, sondern auch ihre Syntax.

Sehen wir uns nun die Textengine-Tokens genauer an.

Textengine-Tokens

Das Modul expression.c implementiert den Typ token_t. Ein Token ist eine Datenstruktur, die die abstrakte Semantik der Textengine darstellt. So wie man mit dem Textenginekern Texteinheiten erstellen und mit Namen versehen kann, so beschreibt jedes Token eine dieser Aktionen. Wäre der Textenginekern als physikalische oder virtuelle Maschine implementiert worden, so wären die Tokens ihre Operationen.

Es gibt diese Arten von Token: UNIT, ENTER, LEAVE, END und NOP. UNIT repräsentiert eine Texteinheit, d.h. es werden ihre Referenzen durch Angabe der Namen der Bezugseinheiten beschrieben. Zum Beispiel besteht das Token ~a :string aus der Referenz für Rolle mit Namen „a” und der Referenz für Typ mit Namen „string”. Die anderen Tokenarten bilden die Operationen des Textkerns ab, respektive te_enter (entspricht dem Zeichen „{”), te_leave (Zeichen „}”) und te_end (Zeichen „;”). NOP ist eine Nulloperation.

Tokenagenten und -quellen

Der abstrakte Typ tokenag_t legt die Schnittstelle für Tokenagenten fest. Der Nutzercode instanziiert einen Tokenagenten und übergibt ihm ein Token mit jedem Aufruf von take_token. Der über new_store_tokenag bereitgestellte Tokenagent führt die im Token enthaltenen Aktionen aus: d.h. ruft die passenden Textkernfunktionen te_set, te_link etc. auf.

Tokenquellen werden als Typ tokensrc_t erfasst und stellen jeweils eine Funktion give bereit, die ein Token für eine angegebene Texteinheit-Id (oder Operation) generiert. Implementiert ist eine Tokenquelle, die die Texteinheiten aus dem Textkorpus zurückgibt. Diese kann man mit der Funktion new_retrieve_tokensrc instanziieren.

Nebenformate

Der Symbolformat-Manager arbeitet eng mit Namen- und Stringformat-Manager zusammen. Der Namens- behandelt die Namen der Einheiten und Referenzen, der Stringformat-Manager die eingebetteten Zeichenketten. Das Namensformat wird im Modul name.c implementiert. Es ist zwar kein Formatmanager im eigentlichen Sinne (es ist kein Subtyp von formatm_t), verfügt aber auch über eine Konfiguration namcf_t und stellt Funktionen zum Parsen und Formatieren bereit. In Zukunft sollte man erwägen, das Namensformat als Format-Manager zu implementieren. Das Namensformat basiert intern auf das Stringformat, denn ein Name besteht aus Bezeichnern, die jeweils eine Zeichenkette sind. Aus diesem Grund enthält die Konfiguration des Symbolsformats symcf_t neben den eigenen Optionen eine Instanz von strcf_t für die Konfiguration der eingebetteten Strings und eine Instanz von namcf_t für die Namen, wobei namcf_t wiederum neben den eigenen Optionen eine weitere Instanz von strcf_t für die Bezeichner besitz. Die Bezeichner-Strings und die eingebetteten Strings können somit unterschiedlich konfiguriert werden. Beispielsweise können Bezeichner mit eckigen Klammern und Substrings mit Anführungszeichen abgegrenzt werden:

~[mein Name] "Max Headroom"

Standardmäßig benutzt das Symbolformat dasselbe Stringformat für beide.

Stringformat

Der Stringformat-Manager ist in stringf.c implementiert. Er ist für die Darstellung von eingebetteten Zeichenketten zuständig und behandelt Anführungs- und Escapezeichen und Leerraum. Siehe in stringf.h die Konfigurationsoptionen unter strcf_t.

Der Stringformat-Manager kümmert sich nur um die Einbettung von Zeichenketten und nicht um die eingebetteten Zeichenketten selbst. Jede Instanz eines Stringformat-Managers hat einen untergeordneten Format-Manager, der die bereinigte Zeichenkette verarbeitet. Beim Parsen der Symbolsprache beispielsweise wird Stringformat für das Parsen der Zeichenketten eingesetzt.

~=a :string; ~a "hallo"; ~=i :integer; ~i "31";

Nach der Verarbeitung durch das Stringformat werden die bereinigten Zeichenketten an unterschiedliche Format-Manager je nach Typ des Erfassten übergeben. So würde oben ein Literalparser die fünf Zeichen h-a-l-l-o und ein Integerparser die zwei Zeichen 3-1 überreicht bekommen.

Für das Parsen und Formatieren vom Typ system.string ist das Literalformat-Manager zuständig, der auch in stringf.c implementiert ist und mit new_stringf_literal instanziiert werden kann. Das Literalformat hat keine Konfiguration, da es definitionsgemäß die Zeichen eins zu eins interpretiert.

Integerformat

Neben Symbol- und Stringformat verfügt die Textengine v0.3 auch über das Integerformat, das in integerf.c implementiert ist. Es unterstützt die Zahlendarstellung beim Parsen und Formatieren. Eine Schilderung der Integerformatierung kann man im Blog finden.

Textengine

Impl: Format

Diese Seite wurde zuletzt 29.09.2019 bearbeitet