std.lfd_parser

Parser für LFD (LyX Form Description) — eine kleine Textsprache, mit der sich Bedienoberflächen beschreiben lassen. Die Unit zerlegt den Quelltext in Token und baut daraus einen begehbaren Syntaxbaum; was daraus wird — Qt-Fenster, Terminaloberfläche, HTML — entscheidet der Aufrufer.

std.lfd_factory · Standard Library · std.alloc

Alle Beispiele dieser Seite sind mit lyxc 1.0.21A übersetzt und ausgeführt; die gezeigten Ausgaben sind echte Programmausgaben.

 
Kein Binärformat. Frühere Fassungen dieser Seite beschrieben LFD als „typisiertes, kompaktes Binärformat — ähnlich MessagePack„. Das trifft nichts davon: LFD ist Klartext, und die Unit ist ein Recursive-Descent-Parser dafür. Auch das @description-Attribut der Unit („LFD binary file format parser“) sagt das Falsche.

Die Sprache

Ein Formular ist ein form-Block; darin stehen Anordnungen, Bedienelemente und deren Eigenschaften. Alle Schlüsselwörter sind klein geschrieben.

form {
  vertical {
    label "Benutzer" { }
    input "" { width: 200 }
    button "Anmelden" { width: 100 height: 30 enabled: true }
  }
}

Aufbau eines Elements: Schlüsselwort, optional ein Text in Anführungszeichen, dann ein Block in geschweiften Klammern. Eigenschaften sind name: wert-Paare.

Kategorie Schlüsselwörter
Wurzel form
Anordnungen layout, vertical, horizontal, grid, stack
Bedienelemente button, label, input, checkbox, radiobutton, combobox, spinbox, slider, listbox, textedit, progressbar, groupbox, tabwidget, splitter, image, custom
Eigenschaften text, tooltip, enabled, visible, width, height
Ereignisse onclick, onchange
Werte Zeichenkette, Zahl, true, false

Ein Name, der in keiner dieser Listen steht, gilt als Bezeichner und ist als Eigenschaftsname zulässig — farbe: 7 wird also angenommen.

 
Die Grammatikdatei lfd-ebnf.md beschreibt eine andere Sprache als der Parser. Dort steht Form Anmeldung "Login" { Layout Vertical { … } } — großgeschrieben und mit Bezeichner zwischen Typ und Titel. Genau das weist der Parser mit -1 und der Meldung expected 'form' ab. Maßgeblich ist die Implementierung; die EBNF ist ein Entwurf (Status „Draft„, v0.1.0).

Parsen und den Baum lesen

import std.lfd_parser;
import std.string;

// Knotentext: Zeiger + Laenge stehen im Knoten (Offsets 8/16), einen Getter
// fuer die Laenge gibt es nicht - deshalb peek64(node + 16).
fn Text(node: int64): pchar {
    var p: int64 := LFDGetNodeText(node) as int64;
    var l: int64 := peek64(node + 16);
    var b: int64 := alloc(l + 1);
    var i: int64 := 0;
    while (i < l) { poke8(b + i, peek8(p + i)); i := i + 1; }
    poke8(b + l, 0);
    return b as pchar;
}

fn main(): int64 {
    var root: int64 := LFDParseString("form { button \"Speichern\" { width: 100 enabled: true text: \"OK\" } }"c);
    PrintLn(StrConcat("root type (0=form): ", IntToStr(LFDGetNodeType(root))));
    var btn: int64 := LFDGetNodeChild(root, 0);
    PrintLn(StrConcat("btn type (6=button): ", IntToStr(LFDGetNodeType(btn))));
    PrintLn(StrConcat("btn text:            ", Text(btn)));
    PrintLn(StrConcat("btn Eigenschaften:   ", IntToStr(LFDGetNodeChildCount(btn))));

    var i: int64 := 0;
    while (i < LFDGetNodeChildCount(btn)) {
        var p: int64 := LFDGetNodeChild(btn, i);
        PrintLn(StrConcat(StrConcat("  ", Text(p)), StrConcat(" = ", IntToStr(LFDGetNodeIntVal(p)))));
        i := i + 1;
    }
    PrintLn(StrConcat("Fehler: ", IntToStr(LFDGetErrorCount(0))));
    return 0;
}

root type (0=form): 0
btn type (6=button): 6
btn text:            Speichern
btn Eigenschaften:   3
  width = 100
  enabled = 1
  text = 0
Fehler: 0

 
Zeichenketten-Werte gehen verloren. In der Ausgabe steht text = 0 — der Parser legt für jede Eigenschaft einen Knoten mit dem Schlüsselnamen als Text an, speichert aber nur Zahlen und Wahrheitswerte (in numVal). Der Wert von text: "OK" wird gelesen und verworfen; er ist über die öffentliche Schnittstelle nicht mehr erreichbar. Wer Texteigenschaften braucht, muss den Quelltext zusätzlich selbst auswerten oder den Parser erweitern.

Knotenmodell

Jeder Knoten ist 48 Byte groß (6 × int64):

Offset Feld Zugriff
+0 Knotenart (LFD_TK_* bzw. LFD_NODE_*) LFDGetNodeType
+8 Byte-Versatz des Textes in der Quelle LFDGetNodeText (liefert den Zeiger)
+16 Textlänge kein Getterpeek64(node + 16)
+24 erstes Kind LFDGetNodeChild(node, 0)
+32 nächstes Geschwister über LFDGetNodeChild(parent, i)
+40 Zahlwert der Eigenschaft LFDGetNodeIntVal

Die Knotenarten sind dieselben Zahlen wie die Token-Arten: LFD_TK_FORM = 0, LFD_TK_VERTICAL = 2, LFD_TK_HORIZONTAL = 3, LFD_TK_GRID = 4, LFD_TK_STACK = 5, dann die Bedienelemente LFD_TK_BUTTON = 6 bis LFD_TK_CUSTOM = 21. Eigenschaftsknoten tragen LFD_NODE_PROPERTY = 101.

Praktisch für eigene Durchläufe: alle Bedienelemente liegen im Bereich LFD_TK_BUTTONLFD_TK_CUSTOM, alle Anordnungen zwischen LFD_TK_LAYOUT und LFD_TK_STACK.

Alias Wert Anmerkung
LFD_NODE_FORM 0 identisch mit LFD_TK_FORM
LFD_NODE_LAYOUT 1 identisch mit LFD_TK_LAYOUT
LFD_NODE_WIDGET 3 Vorsicht: derselbe Wert wie LFD_TK_HORIZONTAL — als Vergleichswert unbrauchbar
LFD_NODE_ROOT 100 wird vom Parser nie vergeben
LFD_NODE_PROPERTY 101 Eigenschaftsknoten
LFD_NODE_EVENT 102 wird vom Parser nie vergeben; onclick erzeugt einen gewöhnlichen Eigenschaftsknoten

Funktionen

Heutige Schnittstelle

Signatur Beschreibung
LFDParseString(input: pchar): int64 Parst den Quelltext und liefert den Wurzelknoten oder -1. Legt Token- und Knotenpuffer neu an; alle Knoten aus einem früheren Aufruf werden damit ungültig.
LFDGetNodeType(node: int64): int64 Knotenart, -1 bei node == 0.
LFDGetNodeChild(node: int64, index: int64): int64 index-tes Kind oder 0.
LFDGetNodeChildCount(node: int64): int64 Zahl der direkten Kinder.
LFDGetNodeText(node: int64): pchar Zeiger auf den Text im Quelltext — nicht nullterminiert. Länge über peek64(node + 16).
LFDGetNodeIntVal(node: int64): int64 Zahl- oder Wahrheitswert einer Eigenschaft (true = 1, false = 0).

Ältere Schnittstelle

Sie stammt aus der Zeit vor LFDParseString und arbeitet auf globalem Zustand; der parser-Parameter wird nirgends ausgewertet — jeder Wert ist zulässig, üblich ist 0.

Signatur Beschreibung
LFDParserNew(sourceText: pchar): int64 Merkt den Quelltext, setzt den Fehlerzähler zurück, liefert immer 1. Parst nicht.
LFDValidate(parser: int64): int64 1, wenn ein nicht-leerer Quelltext gemerkt ist, sonst 0. Prüft die Syntax nicht.
LFDGetLastError(parser: int64): pchar Letzte Fehlermeldung, sonst leerer Text.
LFDGetErrorCount(parser: int64): int64 Zahl der Fehler des letzten Laufs.
LFDParserFree(parser: int64): void Gibt Token- und Knotenpuffer frei und setzt den Zustand zurück.

LFDGetLastError und LFDGetErrorCount gehören zwar formal zur älteren Schnittstelle, sind aber auch nach LFDParseString die einzige Möglichkeit, an Fehlerdetails zu kommen.


Fehlerverhalten

import std.lfd_parser;
import std.string;

fn Probe(name: pchar, quelle: pchar): void {
    var r: int64 := LFDParseString(quelle);
    Print(StrConcat(name, ": "));
    if (r < 0) {
        // Wichtig: bei -1 darf der Wert NICHT an LFDGetNode* weitergereicht werden
        PrintLn(StrConcat(StrConcat("abgewiesen (-1), Fehler ", IntToStr(LFDGetErrorCount(0))),
                StrConcat(", Meldung: ", LFDGetLastError(0))));
        return;
    }
    Print(StrConcat("Kinder ", IntToStr(LFDGetNodeChildCount(r))));
    Print(StrConcat("  Fehler ", IntToStr(LFDGetErrorCount(0))));
    PrintLn(StrConcat("  Meldung: ", LFDGetLastError(0)));
}

fn main(): int64 {
    Probe("klein geschrieben  ", "form { button \"OK\" { } }"c);
    Probe("Gross geschrieben  ", "Form { Button \"OK\" { } }"c);
    Probe("ohne form          ", "invalid { }"c);
    Probe("leerer Text        ", ""c);
    Probe("fehlende Klammer   ", "form { button \"OK\" { "c);
    Probe("negative Zahl      ", "form { button \"A\" { width: -5 } }"c);
    Probe("unbekannte Eigensch", "form { button \"A\" { farbe: 7 } }"c);
    Probe("Eigenschaft ohne :  ", "form { button \"A\" { width 7 } }"c);
    return 0;
}

klein geschrieben  : Kinder 1  Fehler 0  Meldung:
Gross geschrieben  : abgewiesen (-1), Fehler 1, Meldung: expected 'form'
ohne form          : abgewiesen (-1), Fehler 1, Meldung: expected 'form'
leerer Text        : abgewiesen (-1), Fehler 1, Meldung: empty input
fehlende Klammer   : Kinder 1  Fehler 2  Meldung: unexpected token
negative Zahl      : Kinder 1  Fehler 0  Meldung:
unbekannte Eigensch: Kinder 1  Fehler 0  Meldung:
Eigenschaft ohne :  : Kinder 1  Fehler 1  Meldung: expected ':'

Zwei Dinge stehen hier drin, die man leicht übersieht:

  • -1 ist kein Knoten. Wird der Rückgabewert ungeprüft an LFDGetNodeChildCount oder LFDGetNodeType weitergereicht, liest die Funktion von Adresse -1 — das Programm stürzt ab. Immer zuerst auf < 0 prüfen.
  • Ein gültiger Wurzelknoten heißt nicht, dass alles gelesen wurde. Bei fehlender schließender Klammer liefert der Parser einen Baum und zwei Fehler. Nach jedem Parse also zusätzlich LFDGetErrorCount abfragen. Bei mehr als 16 Fehlern in einem Block bricht der Parser die Schleife ab.
 
Ein Minuszeichen wird stillschweigend verschluckt: width: -5 ergibt den Wert 5, ohne Fehlermeldung. Die Zahlenauswertung liest nur Ziffern. Negative Werte lassen sich in LFD derzeit nicht ausdrücken.

Beispiel: Formular auswerten

Der Baum lässt sich rekursiv durchlaufen wie jeder andere auch — hier eine Auswertung, die Bedienelemente zählt und Breiten summiert:

import std.lfd_parser;
import std.string;

var anzahlWidgets: int64 := 0;
var anzahlProps:   int64 := 0;
var summeBreite:   int64 := 0;

fn IstWidget(k: int64): int64 {
    if (k >= LFD_TK_BUTTON && k <= LFD_TK_CUSTOM) { return 1; }
    return 0;
}

fn Text(node: int64): pchar {
    var p: int64 := LFDGetNodeText(node) as int64;
    var l: int64 := peek64(node + 16);
    var b: int64 := alloc(l + 1);
    var i: int64 := 0;
    while (i < l) { poke8(b + i, peek8(p + i)); i := i + 1; }
    poke8(b + l, 0);
    return b as pchar;
}

fn Durchlauf(node: int64): void {
    var k: int64 := LFDGetNodeType(node);
    if (IstWidget(k) == 1) { anzahlWidgets := anzahlWidgets + 1; }
    if (k == LFD_NODE_PROPERTY) {
        anzahlProps := anzahlProps + 1;
        if (StrEquals(Text(node), "width") == 1) {
            summeBreite := summeBreite + LFDGetNodeIntVal(node);
        }
    }
    var n: int64 := LFDGetNodeChildCount(node);
    var i: int64 := 0;
    while (i < n) { Durchlauf(LFDGetNodeChild(node, i)); i := i + 1; }
}

fn main(): int64 {
    var quelle: pchar := "form { horizontal { button \"Ja\" { width: 80 height: 24 } button \"Nein\" { width: 80 } checkbox \"Merken\" { width: 120 enabled: false } } }"c;

    var root: int64 := LFDParseString(quelle);
    if (root < 0) { PrintLn("Parse abgewiesen"); return 1; }
    if (LFDGetErrorCount(0) > 0) {
        PrintLn(StrConcat("Baum unvollstaendig: ", LFDGetLastError(0)));
        return 1;
    }

    Durchlauf(root);
    PrintLn(StrConcat("Bedienelemente:   ", IntToStr(anzahlWidgets)));
    PrintLn(StrConcat("Eigenschaften:    ", IntToStr(anzahlProps)));
    PrintLn(StrConcat("Breite insgesamt: ", IntToStr(summeBreite)));
    LFDParserFree(0);
    return 0;
}

Bedienelemente:   3
Eigenschaften:    5
Breite insgesamt: 280

Ein Durchlauf, der statt zu zählen Widgets erzeugt, wäre die Aufgabe von std.lfd_factory — die ist derzeit eine leere Hülle, weshalb dieser Weg der einzige ist.


Speicher und Lebensdauer

  • LFDParseString belegt zwei Puffer über std.alloc: Token (Quelllänge + 4 Einträge à 24 Byte) und Knoten (Quelllänge / 4 + 16 Einträge à 48 Byte). Für 1 KB Quelltext sind das rund 37 KB.
  • Jeder neue Parse gibt die alten Puffer frei. Knotenzeiger aus einem früheren Aufruf sind danach ungültig — sie zeigen womöglich auf wiederverwendeten Speicher und liefern noch plausibel aussehende Werte. Alte Zeiger nicht aufheben.
  • LFDGetNodeText zeigt in den Quelltext, nicht in einen eigenen Puffer. Der Quelltext muss also mindestens so lange leben wie der Baum; ein Text aus einem freigegebenen Puffer macht alle Knotentexte unbrauchbar.
  • LFDParserFree gibt beides frei. Danach darf kein Knoten mehr benutzt werden.
  • Der Parser hält seinen Zustand global: es gibt immer nur einen Baum gleichzeitig. Zwei Formulare nebeneinander im Speicher gehen nicht — erst auswerten, dann das nächste parsen.

Fallstricke

  • Schlüsselwörter sind klein zu schreiben; Button ist ein Bezeichner, kein Bedienelement.
  • -1 vor jeder Weiterverarbeitung prüfen, danach LFDGetErrorCount.
  • Zeichenketten-Eigenschaften (text, tooltip, onclick) sind über die Schnittstelle nicht auslesbar — nur der Elementtext hinter dem Schlüsselwort ist es.
  • Negative Zahlen werden zu ihrem Betrag.
  • LFD_NODE_WIDGET (3) kollidiert mit LFD_TK_HORIZONTAL (3) — für „ist das ein Bedienelement?“ den Bereich LFD_TK_BUTTONLFD_TK_CUSTOM prüfen.
  • LFDValidate prüft nur, ob überhaupt Text vorliegt — es ersetzt kein Parsen.
  • Kein Kommentarzeichen: alles, was nicht Schlüsselwort, Zeichenkette, Zahl oder Satzzeichen ist, wird als Bezeichner gelesen.

Weiterführend: std.lfd_factory — vorgesehene Widget-Factory · std.alloc · Standard Library

Quelle: std/lfd_parser.lyx · Autor: Andreas Röne · Copyright: 2024–2025 Andreas Röne

Letzte Aktualisierung: 2026-08-13 — Seite gegen std/lfd_parser.lyx neu geschrieben. Die alte Fassung beschrieb LFD als Binärformat „ähnlich MessagePack„ — es ist eine Textsprache für Formularbeschreibungen. Ergänzt: Sprachbeschreibung mit vollständiger Schlüsselwortliste, Knotenmodell samt Offsets, drei lauffähige Beispiele mit echter Ausgabe, das Fehlerverhalten in acht Fällen und die Lebensdauerregeln. Belegt und dokumentiert: Zeichenketten-Werte gehen verloren, -1 führt bei ungeprüfter Weitergabe zum Absturz, ein gültiger Baum kann trotzdem Fehler tragen, -5 wird zu 5, LFD_NODE_WIDGET kollidiert mit LFD_TK_HORIZONTAL. Geprüft mit lyxc 1.0.21A.