Architektur und Funktionsweise
Bis auf wenige Ausnahmen ist der Quellcode für den legacy-interpreter in src/legacy-runtime zu finden.
Zeilenbasierte Behandlung von Skripten und Makros
Grundsätzlich arbeitet der Interpreter zeilenbasiert, da dies auch die Operationsweise des alten Kalibri-Skript ist. Jede Zeile wird dabei als einzeler Buffer abgebildet.
Anbindung an die neue Laufzeitumgebung - legacy-script.ts
In der Datei src/script-api/legacy-script.ts wird die Klasse LegacyScriptEngine implementiert. Die LegacyScriptEngine ist ein Wrapper und bindet den Legacy-Interpreter an die neue Laufzeitumgebung an (Ausführung und Fehlerbehandlung).
Zudem wird in der LegacyScriptEngine die Vorabfrage für die zu prüfenden Parameter durchgeführt, diese als Macro-Steps angelegt und an das User-Interface übergeben, und das Altskript entsprechend der vom Benutzer ausgewählten zu prüfenden Parameter vorgefiltert (ausgeschaltete Parameter werden herausgefilter, aber für Debug-Zwecke die originalen Zeilennummern mitgetrackt, siehe Funktion filterScriptForParameters).
Außerdem werden hier die dem Prüfling statisch zugeordneten Standards requested, damit diese in das Kalibrierprotokoll aufgenommen werden.
Die LegacyScriptEngine ruft dann die Funktion executeLegacyScript auf, wo das Altskript dann tatsächlich ausgeführt / interpretiert wird.
Tatsächlicher Einstiegspunkt des Interpreters - legacy-interpreter.ts
In der Datei src/legacy-runtime/legacy-interpreter.ts befindet sich die Klasse LegacyInterpreter mit dem tatsächlichen Einsteigspunkt des Interpreters executeLegacyScript. In executeLegacyScript werden zuerst die Makro Files vorbereitet, anschließend wird das quasi-globale LegacyState Objekt (Definition in legacy-state.ts) mit Standardwerten initialisiert.
Daraufhin geht die tatsächliche Interpretation des Altskriptes los. Zuerst wird ProcessPreLoop aus src/legacy-runtime/pre-loop/process-pre-loop.ts ausgeführt, welches die Teile vor Hauptschleife des alten Kalibri-Skript (vor dem Label Leseschleife: in GPIB.CPP) übernimmt.
Anschließend wird ProcessLoop aus src/legacy-runtime/loop/process-loop.ts aufgerufen. Dies entspricht der Leseschleife des alten Kalibri-Skript, in welcher die Interpretation des Skriptes stattfindet. Zum Abschluss wird ProcessPostLoop aufgerufen.
ProcessPreLoop
In ProcessPreLoop aus src/legacy-runtime/pre-loop/process-pre-loop.ts wird der Protokollkopf interpretiert und die Basic Variablen genullt.
Main-Loop
ProcessLoop aus src/legacy-runtime/loop/process-loop.ts ist die Hauptschleife des Interpreters. Es entspricht der Leseschleife des alten Kalibri-Skript.
Diese Schleife arbeitet zeilenbasiert das Altskript (Haupt-File / VORLAGE.PRO oder auch Makros) ab.
Mittels readLine wird die nächste zu verarbeitende Zeile abgerufen. Anschließend wird geprüft ob eine Zeile abgerufen werden konnte (falls nicht ist das Skript zu Ende, ohne dass es durch $$ korrekt beendet wurde). Leere Zeilen werden übersprungen.
Danach wird geprüft ob es sich bei der Zeile um einen Parameter (@::) handelt, und falls ja und es sich bei dem aktuellen File um das Haupt-File / Main-Script handelt dies an das User Interface kommuniziert, damit die auf Makro-Steps umgesetzten Parameter im User Interface korrekt angezeigt werden (in welchem Parameter / Makro-Step befindet sich das Skript aktuell).
Anschließend wird geprüft ob es sich um eine Beendigung des Skriptes handelt ($$).
Daraufhin wird geprüft ob es sich um eine Instruktion / Kommando handelt (Zeile beginnend mit @). Falls ja wird die Zeile mittels processInstructionLine ausgeführt.
Danach wird geprüft ob es sich bei der Zeile um Kalibrieranweisungstext handelt und falls ja, diese entsprechend an das User-Interface kommuniziert.
Hiernach wird geprüft, ob es sich bei der Zeile um eine manuelle Ausgabe in das Protokoll handelt und diese entsprechend an den Virtual Interpreter weitergegeben.
Schlussendlich (quasi als Fallback) nimmt Kalibri-Skript an, dass es sich bei der Zeile um eine Tabellenzeile handeln muss. Diese werden mittels ProcessTable verarbeitet.
processInstructionLine
processInstructionLine ist das Herzstück des Interpreters. In dieser Funktion werden Befehle / Instruktionen abgearbeitet. Dies passiert durch die möglichst genaue Nachbildung der Logik aus Kalibri-Skript (bspw. suchen nach @ da mehrere Befehle in einer Zeile sein können).
Mittels tryMatchAndInvokeInstruction wird versucht die Instruktion abzuarbeiten.
Instruktionen
Um eine einzige gigantische Datei (“Spaghetti-Code”) mit allen Befehlen wie im alten Kalibri-Skript zu vermeiden wurde jede Instruktion in eine einzelne Datei verpackt. Zur besseren Wartbarkeit wurden im Gegensatz zum alten Kalibri-Skript, wo sich alle Instruktionen in einer Datei (“Spaghetti-Code”) befinden, jede Instruktion in eine einzelne Datei verpackt und sauber gekapselt.
Die Basis für Instruktionen bildet die Klasse Instruction aus src/legacy-runtime/loop/instruction.ts. Eine Instruction besteht aus einem InstructionMatcher und einem LegacyInstructionHandler.
Der InstructionMatcher wird mit einem Buffer aufgerufen und gibt zurück, ob für die zugehörige Instruktion eine Übereinstimmung vorlegt. Dies entspricht dem Verhalten des alten Kalibri, wo mit if(gleich(Text,"...")) bzw. if(gleich(Text+x,"...")) geprüft wird ob eine Übereinstimmung vorlegti.
Der LegacyInstructionHandler entspricht dem tatsächlichen Code der Instruktion. Ein LegacyInstructionHandler wird mit LegacyContext aufgerufen und gibt ein LegacyCMDProcLoopReturnCode zurück.
LegacyContext enthält das quasi-globale LegacyState Objekt und einen Injector. Der Injector wird für Dependency Injection genutzt, d.h. zum Abrufen von Schnittstellen für Kommunikation mit Hardware wie GPIB, Kommunikation mit dem User-Interface, usw…
LegacyCMDProcLoopReturnCode ist die Rückgabe der Instruktion und gibt an, was die Schleife in processInstructionLine nach Ausführung der Instruktion machen soll. Im Alten Kalibri-Skript hat jeder Befehl ein continue, break oder return. Dies bildet LegacyCMDProcLoopReturnCode nach. LegacyCMDProcLoopReturnCode kann entweder break oder continue sein. Diese entsprechen direkt den break oder continue aus dem alten Kalibri-Skript. Bei break wird keine weitere Instruktion in der Zeile ausgeführt. Aufgrund der uneindeutigen Syntax des alten Kalibri-Skript ist dies notwendig und auch im originalen Kalibri-Skript so der Fall, d.h. manche Instruktionen müssen die letzte in einer Zeile sein, auf andere Instruktionen können in derselben Zeile weitere Instruktionen folgen. Bei continue wird nach der nächsten Instruktion in der Zeile gesucht.
return wurde nicht als Rückgabe abgebildet. return wurde im alten Kalibri-Skript zur Fehlerbehandlung genutzt. Im neuen Kalibri-Skript wurde dies gemäß modernen Standards mittels Exceptions gelöst.
Die verfügbaren Instruktionen werden in src/legacy-runtime/loop/instructions.ts als Array registriert. Durch dieses Array iteriert tryMatchAndInvokeInstruction durch.
tryMatchAndInvokeInstruction
Die Funktion tryMatchAndInvokeInstruction iteriert durch das instructions Array aus src/legacy-runtime/loop/instructions.ts und prüft der Reihe nach für jede Instruktion, ob der Matcher der zugehörigen Instruktion eine Übereinstimmung angibt. Falls eine Übereinstimmung vorliegt wird der dazugehörigen handler ausgeführt.
Diese Vorgehensweise entspricht der Funktionsweise des alten Kalibri-Skript. Aufgrund der uneindeutigen Syntax ist dies alternativlos, um eine korrekte Ausführung der Skripte zu gewährleisten.
Deswegen ist auch die Reihenfolge der im instructions Array registrierten Instruktionen höchst relevant, darf nicht verändert werden und muss der Reihenfolge der if(gleich(Text... aus dem alten Kalibri-Skript entsprechen! Da “Überlappungen” bei den Befehlen vorliegen, muss die Reihenfolge stimmen.
Virtual Interpreter
Der Virtual Interpreter ist für die Live-Interpretation des Kalibrierprotokolls zuständig und bildet die Schnittstelle zwischen den Altskripten und dem alten Kalibri-Skript und den neuen Kalibrierscheinen und ihrer API. Mehr Informationen zu den Gründen für diese Entscheidung finden sich im Kapitel über Designentscheidungen.
Der Virtual Interpreter findet sich in src/legacy-runtime/virtual-protocol/virtual-protocol.ts, der Einstiegspunkt ist writeToVirtualProtocol.
writeToVirtualProtocol prüft zuerst ob schon vollständige Zeilen zum Verarbeiten vorliegen, denn es ist möglich und wird so praktiziert Zeilen Stück für Stück in das Protokoll zu schreiben und erst nach mehreren Schritten mit einem Zeilenumbruch zu terminieren. Eine Zeile kann nicht sinnvoll verarbeitet werden, wenn sie noch nicht mit einem Zeilenumbruch terminiert ist.
Liegen vollständige Zeilen zum Verarbeiten vor werden diese in processVirtualProtocolLine verarbeitet.
Der Virtual Interpreter ist lose als State-Machine implementiert, es gibt dabei verschiedene Statusse wie VPISXX (VPIS = Virtual Protocol Interpreter State).
Diese Statusse stellen den aktuellen Status des Protokolls da, bspw. ob sich das Protokoll gerade innerhalb einer Tabelle befindet.
Die Hauptarbeit, also das wirkliche Interpretieren des Protokolls, findet also in processVirtualProtocolLine statt, hier ist die State Machine implementiert.
Tabellen werden in addTableToProtocol, processTableRow und processTableStart verarbeitet.
Tabellen / ProcessTable
Tabellen stellen einen essentiellen Teil von Kalibri-Skript dar. Die komplette Verarbeitung von Legacy-Tabellen findet im Virtual Interpreter statt. Alle Befehle welche mit Tabellen interagieren, bspw. der Instruction Handler für @Istwert=..., erzeugen Ausgaben ins Virtuelle Protokoll bzw. den Virtual Interpreter, damit diese Verarbeitung von Tabellen bzw. Umsetzung auf die neue Tabellen-API zentralisiert ist.
Für Tabellen welche regulär über die alte Kalibri-Skript Tabellenfunktionalität erstellt werden findet die Verarbeitung in src/legacy-runtime/loop/tabelle/process-tabelle.ts bzw ProcessTable statt, welches dann wiederum Zeilen in das Virtuelle Protokoll bzw. an den Virtual Interpreter gibt.
Der Großteil der Logik für die Tabellen selbst ist in src/legacy-runtime/table/legacy-table.ts, bspw. die Umsetzung der alten Tabellen-Spezifikationen wie Toleranzen auf die neue API, Berechnung von Toleranzbereichen nach der alten überholten Logik, etc.
Makros
Makros werden ebenfalls in der Main-Loop ausgeführt, wie das normale Hauptskript / VORLAGE.PRO. Hierzu kann über context.state.currentOpenFile verändert werden, aus welchem virtuellen File (Hauptskript oder Makro) die zu lesenden Zeilen abgerufen werden.
Dies entspricht dem Vorgehen des alten Kalibri-Skript, nur das statt mit normalen iostreams auf Basis von “virtuellen” Dateien gearbeitet wird.
Makro Files werden executeLegacyScript vorverarbeitet (bspw. Splitten in Zeilen). Die Logik für den Übergang / Aufruf von Makros ist eine normalen Instruktion in macroEqInstructionHandler / src/legacy-runtime/loop/instructions/1387-macroGleich.ts.
Call Stack
Der alte Call-Stack (DateiZeigerGosub und stapelZeiger) ist in LegacyState in dateiZeigerGosub abgebildet. Die Funktionalität wurde um Fehlerbehandlung erweitert.
Das alte Kalibri-Skript hat in DateiZeigerGosub lediglich Zeilennummern gespeichert. Hier war der Fehler möglich, dass bei falscher Benutzung von Makros in eine Zeilennummer gesprungen wurde, welche zu einer komplett anderen Datei gehört. Zudem konnte auch durch Fehler in den Skripten auf Teile des Call-Stacks zugegriffen werden, die eigentlich gar nicht mehr gültig ist.
Dieser Umstand wurde im neuen Interpreter behoben. In dateiZeigerGosub wird zusätzlich zur Zeilennummer auch die Datei gespeichert, zu welcher die Zeilennummer gehört. Falls die aktuell “geöffnete” virtuelle Datei nicht zur Zeilennummer passt wird eine Fehlermeldung erzeugt.