Einführung in die Informatik

Altkausuren

Teil 1: Einführung

bro so was soll ich dazu überhaupt schreiben idk
Code ist eine Anreihung von Anweisungen dings

Teil 2: Objektbasierte Programmierung

Kapselung

Abstraktionsebene zwischen Dingen außerhalb und innerhalb von Klasse (z.B. Getter und Setter) erleichtert Wartung und Modifikationen am gekapselten Stuff.

Primitivity

Primitive Non-Primitive
Beispiele int, boolean String, Array, jede custom class
Wert direkt die Daten selbst eine referenz auf die Adresse mit den Daten
Als Methodenparameter Methode erhält Kopie des Werts, kann den "äußeren" Wert nicht ändern Methode erhält Kopie der Referenz (kann also die Daten dennoch ändern (⚠️ aber nicht bei reassignment! und nicht bei String, da dieser immutable ist).

Modulo-Operator

Hier gibt es 1 wichtiges Detail, das relevant werden kann:

in Mathe in Java
13 % 6 == 1 13 % 6 == 1
-13 % 6 == 5 (in Restklasse) -13 % 6 == -1
In Java hat das Ergebnis von a % b immer dasselbe Vorzeichen wie a (der Dividend). In der Mathematik ist der Rest hingegen meistens so definiert, dass er immer ≥0 ist.

Vergleichsoperatoren

Pre-/Post-Increment/Decrement

a++/a-- Wert von a gelesen und im Execution Stack gespeichert, dann wird a in Memory in-/dekrementiert.
++a/--a a wird in-/dekrementiert, dann wird der Wert gelesen.
Deswegen liebe ich EIDI

Natürlich verwöhnt uns Big P hier wieder mit dem EIDI-Classic:

"Having to understand code that would instantly get you fired at a real job"

Da beim Assignment a=a++ der Wert von a zuerst im Execution Stack gespeichert wird, und die anschließende In-/Dekrementierung in Memory gespeichert wird, wird beim anschließenden Reassignment von a noch der alte Wert vor der Änderung aus dem Execution Stack verwendet. Die Anweisung a=a++ ändert also nicht den Wert von a. (Keine Sorge du musst nicht wissen was ein Execution Stack ist.)

Decorators

Access Modifier

Modifier Klasse #Package Unterklasse Welt (Global)
public
protected
(kein Keyword)
private

Other Keywords

decorator function
abstract - Methoden: Deklarierung ohne Implementierung
- Klassen: nicht instanziierbar, also es können keine objekte von ihnen erstellt werden (see also: Interfaces)
- Alle Klassen, die mindestens eine abstract Methode haben, müssen abstract sein
final - Variablen
- primitives: Konstanter Wert (kann nicht in runtime geändert werden)
- non-primitives: nur der Wert, also die Referenz ist final, nicht die Daten auf die sie zeigt
- Klassen: kann nicht extended werden
static - Gehört nicht dem jeweiligen Objekt, sondern der Klasse an sich
- Kann ohne bestimmte Referenz auf oder Existenz eines Objekts verwendet werden
- Useful für Konstanten (mit static) oder id (static int count=0; int id=count++;[1])
default Nur in #Interfaces möglich, entfernt sozusagen das hier automatisch anwesende abstract, sodass wir eine Implementierung für die Methode angeben können.

Package

Call-by-Value (Referenzen)

Bei Non-Primitive Methodenargumenten übergibt Java Kopien der Referenz. Modifizierung des Objekts (z.B. arr[0]=1) ändert das Original-Objekt im Heap. Reassignment der Variable auf (neues) Objekt (z.B. arr = new int[1]) ändert nur die lokale Kopie der Referenz und hat außen keine Auswirkung.

Teil 3: Testen

Die Idee

Die Idee (bei Unit-Tests, eigentlich gibts auch noch andere wie z.B. Implementation Tests aber nicht Stoff) ist folgende:
Wir zerhacken unseren Code in die minimalen Bestandteile, idealerweise Funktionen ohne Side-Effects (nur Input und Output) und schreiben für jede Tests, bei denen wir Inputs providen und einen bestimmten Output dafür erwarten. So wissen wir direkt, wo genau der Fehler liegt. Wir schreiben sowohl Tests für das Base-Case (erwartbare Inputs), als auch für Edge Cases (weniger erwartbare Inputs). Eine gute Weise die Test-Cases zu strukturieren sind sogenannte Testklassen, bei denen man mit einer Reihe von Fallunterscheidungen alle möglichen Kombinationen und so alle Situationen abdeckt.

Welcome to Reality

Meistens geht das mit dem Zerhacken nicht so schön, weil viele Methoden in der echten Welt inheränt Side-Effects haben (außer man ist auf Hascell-FP-Trip). Ich muss z.B. auf Arbeit oft Unit Tests schreiben für Methoden die auf ne DB zugreifen. Oder die Methode ist abgesehen von den Parametern noch von anderem Stuff abhängig, wie z.B. Datum. Hier braucht man dann Mocking, wodurch man versucht, möglichst nur die Methode im Test zu testen und alles andere zu imitieren und damit abzuschotten.

Syntax

@Test  // Test annotation
public void testForBitFlip(){
    int a = 6;
    int b = 7;
    int c = a+b;
    assertEquals(c,13); // assert-Keyword (there's a bunch)
}
Niemals Non-Primitives mit == vergleichen!

Dann vergleichst du nur deren Referenzen du Dulli. Mach einfach asssertEquals() du Dulli.

Teil 4: Kontrollstrukturen

broo free, außer maybe die hier:

Ternary Operator

a = b ? c : d

ist äquivalent zu

if (b)
{
	a=c;
}
else
{
	a=d;
}

Switch-Case

-Statement

switch (a)
{
	case 1:
		System.out.println("eins");
		break;
	case 2:
		System.out.println("zwei");
		break;
	case 3: case 4:
		System.out.println("drei oder vier");
		break;
	default:
		System.out.println("out of bounds");
}

Ohne die break Statements, würde danach einfach das nächste Case getestet werden, quasi als wären es einfach lauter ifs (und default eine normale Anweisung). Das wäre hier z.B. ein Problem, wenn statt blablab() der Wert von a auf 2 gesetzt wird.

-Expression

Das ganze geht auch fancier, und zwar wenn es keine Anweisung, sondern ein Ausdruck sein soll:

System.out.println(
	switch (a)
	{
		case 1 -> "eins";
		case 2 -> "zwei";
		case 3,4 -> "drei oder vier";
		default -> "out of bounds";
	}
)

Dann braucht man wegen dem -> auch keine breaks.

do-while

Der Böse Couseng von while, voll praktisch manchmal:

do 
{
	userInput = getUserInputOrSmth();
}
while (userInput.isInvalid());

Die Schleife wird halt immer mindestens 1x ausgeführt, bevor die Condition überhaupt gecheckt wird.

Enums

Nützlich, wenn es eine begrenze Anzahl an Belegungen geben soll für Variable.

enum Beziehungsstatus
{
	LEDIG, LIIERT, VERHEIRATET, VERWITWET
}
Beziehungsstatus b = Beziehungsstatus.VERHEIRATET;
Beziehungsstatus[] alleOptionen = Beziehungsstatus.values()

Kontrollflussdiagramm

Beispiel:

flowchart TD
    S([Start]) --> IN[/"Eingabe: x = read();"/]
    IN --> A["Zuweisung/Anweisung: sum = 0; i = 0;"]

    %% ---------- Schleife (while) ----------
    A --> W{"while-Bedingung: i < n ?"}
    W -- "yes" --> BODY["Schleifenrumpf: sum = sum + a[i];"]
    BODY --> INC["Anweisung: i = i + 1;"]
    INC --> W
    W -- "no" --> AFTER_LOOP((Nach der Schleife))

    %% ---------- switch-case (Mehrfachselektion) ----------
    AFTER_LOOP --> SW{"switch(command)"}
    SW -- "case 'n'" --> CNEW["createNewFile();"]
    SW -- "case 'o'" --> COPEN["openFile();"]
    SW -- "default" --> CDEF[/"Ausgabe: Unbekanntes Kommando"/]

     Pfeile zu sich selbst
    S -- "extends (≤1)" --> S
    IN -- "extends (n)" --> IN

Polymorphie

Statischer vs. Dynamischer Typ

StaticType o = new DynamicType()
Der statische Typ bestimmt die sichtbaren Methodensignaturen für den Compiler; der dynamische Typ bestimmt die ausgeführte Implementierung der JVM.

Methodensignatur

Name (nicht Return-Type), Parametertypen und Reihenfolge (nicht Namen)
aka public static int method(String hallo, int hallo)

Generics & Bounds

Bei A<U extends C> ist die Konstruktion new A<C>() erlaubt, da für die Typprüfung und Generics jede Klasse als Subtyp ihrer selbst betrachtet wird.

Casting

oldStaticType object = new dynamicType();
newStaticType object = (newStaticType) object;

Erstellt ein neues Objekt mit dem alten dynamischen und dem in Klammern angegebenen neuen statischen Typ.
Wenn neuer statischer Typ in der Vererbungshierarchie unter dem dynamischen liegt, gibts ClassCastException.
Ein Upcast (von extender auf extendee) schränkt die Sichtbarkeit auf Methoden ein, die im statischen Zieltyp deklariert sind.

Implizit vs Explizit

Entlang der Vererbungsrichtung (in Pfeilrichtung) castet Java implizit aka automatisch (da keine Daten verloren gehen).
Andersrum muss man explizit casten.
Das gilt auch bei primitiven Typen.
!Polymorphie, p.1

Und welche scheiß Methode wird jetzt aufgerufen?

!Polymorphie, p.2
In other words: Such bei statischem Typ (oder wenn nichts gibt bei dessen Oberklassen) die Methode raus, die am besten passt. Dann nimmt die in der Vererbungshierarchie möglichst nah am dynamischen Typen liegende Implementierung dieser Methode. (Und zwar mit exakt der selben Signatur!)

Best Fitting Methode

Zunächst werden alle Methoden ausgesucht, deren #Methodensignatur auf den Call passt (auch wenn dafür implizite Parametercasts notwendig sind).
Für jeden supplied Parameter wird geschaut, welche dieser Methoden diesen mit den wenigsten impliziten Casts annehmen kann. Gibt es eine Methode, die für alle Parameter der beste Fit ist, wird sie gewählt, sonst ein Ambiguity Error geworfen.

#Teil 8: Rekursion

Holy shit warum verbringen wir soviel Zeit mit Rekursion

<rant>
du wirst das nie in deinem Job brauchen
Die wichtigsten Anwendungen für Rekursion finden sich bei Giga-Low-Level Zeug wie Linked Lists oder #Merge Sort. Also Zeug das schlaue Leute vor 30 Jahren schon für dich gemacht haben! Du wirst auf Arbeit nie ein Ticket assigined bekommen "bitte bau einen custom sortalg" dikkahhh
</rant>

Aufbau

Backtracking

Die Idee is basically, dass du nicht immer alle rekursiven Branches betrachten musst. Wenn du z.B. nach etwas suchst, kannst du als return-value ne boolean nehmen, die der Methode Bescheid sagt, ob ihre rekursiven Aufrufe was gefunden haben. So kann bei Fund sofort das Programm beendet werden.

End-Rekursion

Hey Leute, ich weiß wir haben grade 2 Wochen gelernt, warum Rekursion so viel besser als Iteration ist

Ah btw, wenn möglich schreibt eure Rekursion so, dass sie der Compiler einfach als Iteration umformen kann 😭😭😭

Wenn der Rekursive Aufruf nach der Rechnungslogik der Funktion steht, muss beim Erreichen von ihm nicht die Return-Adresse modifiziert werden, da wir nicht mehr an diese Stelle returnen müssen (es gibt ja nach dem Rekursiven Aufruf nichts mehr zu tun). Deshalb können wir die einfach die ganze Zeit an der Stelle lassen, wo die Funktion das erste Mal aufgerufen wurde. beim Erreichen der Abbruchbedingung springen wir dann direkt ganz raus (Tail Call). So sparen wir uns ne Menge Platz im Stack weil sonst müssten wir ja pro Rekursivem Aufruf eine Rücksprungadresse und Werte für die Berechnung später reinschreiben. Stattdessen machen wir die Berechnung jetzt einfach vor dem Aufruf/im Parameter.

static int factorialRecursive(int n) 
{
	// 1 Base-Case
    if (n <= 1) 
        return 1;
        
    // 2 Rekursiver Aufruf
	int preResult = factorial(n - 1); //Hierhin muss zurückgesprungen werden
	
	// 3 Calculation
    return n * preResult; // Erst jetzt beim Weg heraus wird auf preResult draufmultipliziert
}

vs.

static int factorialTailRecursive(int n, int preResult)
{
	// 1 Base-Case
	if (n <= 0)
		return preResult;

	// 2(!!) Calculation
	preResult = preResult * n; // preResult wird schon beim Weg hinein draufmultipliziert
	
	// 3(!!) Rekursiver Aufruf
	return factTR(n-1, preResult); 
}

but wait... das würde in Assembly dann etwa so aussehen

START_METHOD:

    # 1 Base-Case
    COMPARE n, 0
    JUMP_IF_LESS_EQUAL FINISH

    # 2 Calculation
    MULTIPLY preResult, preResult, n
    
    # 3 Rekursiver Aufruf
    SUBTRACT n, n, 1
    JUMP START_METHOD

FINISH:
    RETURN preResult

sieht das nicht ein bisschen aus wie...

static int factorialLoop(int n, int preResult) {
    // 1 Base-Case
    while (n > 0) { 
        
        // 2 Calculation
        preResult = n * preResult; 
        
        // 3 """Rekursiver Aufruf"""
        n = n - 1;
        // (jump passiert automatisch)
    }
    return preResult;
}

...

Divide and Conquer

Die Idee

Probleme solange zerhacken bis sie trivial sind (wünschte das ginge auch irl 🚬)

Beispiele

Die Idee des Algorithmus ist wie folgt (unter der Annahme, dass die Menge in aufsteigender Reihenfolge sortiert ist):

Merge Sort

hatten wir schon

Teil 9: Fortgeschrittene Programmierkonstrukte

Iteration

Lambda Expressions

aus: kann man machen: Erklärung
dtyp methode(dtyp param, ...)
{ stuff }
(dtyp[2]param, ...) -> {stuff [..] return wert} kein methodenname nötig, aber dafür pfeil
(...)
{ return wert }
(...) -> wert nur 1 anweisung: direkt return
dtyp methode(dtyp param)
{...}
param -> {...} nur 1 parameter
:: Methode einer Klasse/static Objekt auswählen

Berühmte Interfaces (aus java.util.function)

Interface Funktion Erklärung / Logik
Supplier<T> T get() Liefert einen Wert vom Typ T.
Consumer<T> void accept(T a) Nimmt einen Wert T an und macht "shit" damit, ohne etwas zurückzugeben.
Function<T, R> R apply(T a) Nimmt T an und gibt ein Resultat R zurück.
Predicate<T> boolean test(T a) Prüft eine Bedingung und gibt true oder false zurück.
Comparator<T> int compare(T a, T b) Vergleicht zwei Objekte:
a > b -> return > 0
a == b -> return 0
a < b -> return < 0

Streams

Erstellen

Stream-Methoden

Genau eine terminale Operation pro Stream!

Stream kann nur eine terminale Operation bekommen, aber erst durch diese werden die intermediate Operationen überhaupt erst ausgeführt!

Intermediate

Methode Erklärung
map() nimmt Funktion T -> T macht shit auf jedem Element des streams und gibt neuen zurück (Function). Datentyp ändern mit #Casting oder auch z.B. mapToInt() bei supported Typen (dann kriegt man auf Datentyp ausgerichteten Stream mit extra Methoden wie avg()). Braucht terminale Option zum funktionieren.
flatMap() nimmt Funktion T -> Stream<T>, aber tut den Inhalt jedes Streams hintereinander in den neuen Stream so können pro Wert >1 neue Werte entstehen.
filter() filtered nach Predicate.
takeWhile() nimmt Werte weiter solange Predicate zutrifft, dann stopp.
limit(n) nimmt die ersten n Elemente.
distinct() self explanatory.
peek() wie foreach() aber consumed nicht -> braucht terminale Option zum funktionieren.
sorted() wenn Elemente nicht Comparable sind braucht es Comparator, es gibt standard aber man kann auch seinen eigenen machen.
parallel() parallele Bearbeitung, Reihennfolge nicht vorhersehbar!

Terminal

Methode Erklärung
foreach() macht generell shit ohne return, aka nimmt einen Consumer
collect() in Collection speichern (z.B. Collectors.toList()).
toArray() kann Referenz für Konstruktor der Klasse nehmen.
count() self explanatory.
findAny() beliebiges Element finden (als Optional).
max/min(comparator) findet das Maximum oder Minimum basierend auf dem Comparator.
reduce(startWert,acc) reduziert auf einen Wert (using accumulator, erst Startwert mit Element 1, dann das Ergebnis mit Element 2, ...).
Consumer anwenden ohne Termination

Um foreach()-Style Operationen vorzunehmen (z.B. Attribute der enthaltenen Objekte modifizieren) ohne den Stream zu terminieren (weil man danach noch z.B. sortieren will), kann man stattdessen Operationen wie map() oder peek() nutzen. Diese werden aber logischerweise nur ausgeführt, wenn der Stream später noch terminiert wird.

Speichereffizienz

Streams sind lazy und verarbeiten Daten "on-the-fly", was sie speichereffizienter macht als persistente Collections im RAM.

I/O

You better catch that shit

Oft muss bei diesen Operationen eine IOException gecatched werden

Buffers

Zwischenlager zwischen Produzent und Konsument im Allgemeinen. Bei #I/O nützlich da wir die Datei nicht Byte für Byte lesen müssen.

Output: 01001101 01100101 01110010 01101100 01101001 01101110

Ohne Buffer Mit Buffer
0-> hm ok, interessant 01001101-> M
1-> aha crazy... 01100101-> E
0-> wow, wieder ne Null? 01110010-> R
0-> ohaa noch eine? 01101100-> L
1-> fuck jetzt wieder ne eins? 01101001-> I
1-> damnn noch eine? 01101110 -> N

Socket

Können Verbindung zwischen mehreren Rechnern herstellen. Dann ist ein Datentransfer via InputStream und OutputStream möglich.
Um eine Verbindung zu einem anderen Rechner aufzubauen, benötigt man dessen Hostname (IP-Adresse oder Domain) und den Port (0–65535). Beim Instanziieren mit new Socket(host, port) wird die Verbindung automatisch aufgebaut.

Reader & Writer

Typ Klasse Funktion
Basic InputStreamReader / OutputStreamWriter Konvertiert Bytes in char / String und umgekehrt.
Effizient BufferedReader / BufferedWriter Nutzt #Buffers, um Datenblöcke statt einzelner Zeichen zu verarbeiten. Erlaubt readLine() oder .lines().
Komfort PrintWriter Kann beliebige Objekte via toString() direkt in den Stream schreiben (print(), println()).

Server Sockets

Ein Server "spricht" nicht von sich aus, sondern "lauscht".

  1. Instanziierung: ServerSocket server = new ServerSocket(port);
  2. Warten: Socket client = server.accept();
    • Diese Methode ist blockierend: Der Thread bleibt stehen, bis sich ein Client verbindet.
    • accept() gibt dann ein normales Socket-Objekt für die Kommunikation mit diesem Client zurück.
Ressourcen-Management Sockets, Writer und Reader müssen am Ende immer mit .close() geschlossen werden, damit der Port wieder freigegeben wird und das Programm nicht beim Beenden blockiert.

Error Handling

Netzwerkoperationen sind fehleranfällig. Fast alle Methoden werfen eine IOException.

Exceptions/Errors

classDiagram
    class Throwable {
        <>
    }
    class Error {
        +OutOfMemoryError
        +StackOverflowError
    }
    class Exception {
        <>
        +IOException
        +SQLException
    }
    class RuntimeException {
        <>
        +NullPointerException
        +ArithmeticException
        +IndexOutOfBoundsException
    }

    Throwable <|-- Error
    Throwable <|-- Exception
    Exception <|-- RuntimeException

Checked bedeutet, dass man sie entweder catchen oder mit throws nach oben weitergeben muss.
Alle RuntimeException sind unchecked, alle anderen Exceptions checked. Dann gibt es auch noch Errors, die kann man nicht abfangen weil danach wallah Krise ist (z.B. StackOverflowError).
Bei try{} catch(Exception e){} finally{} ist die Funktion von finally, dass es fast immer ausgeführt wird, egal ob eine ungegangene Exception geworfen wurde oder ob im try/catch ein return steht.
Sinn: Aufräumarbeiten (Files schließen, Sockets beenden), damit die nicht offen bleiben, wenn es knallt.

FAST immer

Die Ausnahme: wenn System.exit(0) gerufen wird oder die JVM komplett abschmiert (Stromausfall type shit)

Teil 10: Nebenläufigkeit

Threads

Thread vs Runnable

Runnable

Thread

kann Runnable übergeben bekommen, um zu festlegen was er tun soll

Verwendung von Threads

zum Starten IMMER start() benutzen, nie run()

Wartende Methoden

Unterbrechende Methoden

Keywords

Producer-Consumer-Problem

Ein klassisches Problem der Nebenläufigkeit:
2 Prozesse, die mit einem Pufferspeicher gekoppelt sind. Einer produziert etwas, einer konsumiert es, aber sie arbeiten nicht gleichschnell. Das kann zu Problemen führen, wenn der Puffer voll ist und der Producer weiter producen will oder der Buffer leer ist und der Consumer weiter consumen will.

Typische Problem-/Lösungsfolge

Mehrere Prozesse greifen auf Ressource zu

⬇️

Race Condition

Datenverfälschung durch zeitgleichen Zugriff
(z.B. Beide lesen erst ob ein Sitzplatz noch buchbar ist und reservieren ihn dann.)

⬇️

Monitor

synchronized
Blockierung von Zugriff auf Ressource, wenn schon auf sie zugegriffen wird
(z.B. Bei Klick auf Sitzplatz wird nicht nur gecheckt ob er schon reserviert ist, sondern auch ob grade jemand schon im Begriff ihn zu buchen ist, dann ggf warten auf Freigabe)

⬇️

Deadlock

Mindestens zwei nebenläufige Prozesse befinden sich in zyklischer Wartesituation auf exklusiv und ununterbrechbar von einem anderen Prozess belegte, gemeinsame Ressourcen.
Pasted image 20260204153203.png

⬇️

Lösungsmöglichkeiten

Semaphor, Lock, etc.

Semamphore(n) mit n als Anzahl Permit

  • acquire() nimmt 1 Permit, wartet falls 0 ist
  • release() gibt 1 Permit zurück

Teil 11: GUIs

GUIs sind Graphical User Interfaces. Eine Anwendung besteht typischerweise aus einem Frame (Hauptfenster) und verschiedenen Swing-Komponenten (z.B. „ein Knopf“ und weitere einfache Bedienelemente), die über Ereignisse (z.B. Klicks) gesteuert werden. Für die Anordnung der Komponenten sind Layout-Konzepte zentral (behandelt als Layout I und Layout II), und als größere Anwendung wird die Kombination von GUIs mit Netzwerkprogrammierung thematisiert.

Highly doubt, dass irgendwas davon drankommt

Teil 12: Compiler

Compiler-Phasen

1) Scanner (lexikalische Analyse)

Aufgabe: Quelltext wird in Tokens zerlegt. Identifiziert u.a. reservierte Wörter, Namen, Konstanten
Ignoriert: White Space und Kommentare Technik: Vergleich mit regulären Ausdrücken, daraus werden „Wörter“ (= Tokens).

2) Parser (Syntaxanalyse)

Aufgabe: Analysiert die Struktur des Programms.
Ergebnis: Erzeugt einen Syntaxbaum und prüft, ob die Syntax korrekt ist.

3) Semantische Analyse

Aufgabe: Prüft Kontextbedingungen, z.B. ob Variablen vor ihrer Verwendung deklariert wurden.

4) (Zwischen-)Codegenerierung

Aufgabe: Aus (annotiertem) Baum wird (Zwischen-)Code erzeugt.

5) Optimierung

Aufgabe: Der erzeugte Code wird abschließend optimiert.

Bytecode

Was steht „drin“? (Instruktionen)

Befehl Parameter Bedeutung
NEG int negieren (Vorzeichenwechsel)
ADD zwei int addieren
SUB zwei int subtrahieren
MUL zwei int multiplizieren
DIV zwei int dividieren
NOT boolean negieren
AND logisches UND
OR logisches ODER
LESS Vergleich < (liefert boolean)
LEQ Vergleich <= (liefert boolean)
EQ Vergleich == (liefert boolean)
NEQ Vergleich != (liefert boolean)
CONST i i (int) Konstante i auf den Stack legen
TRUE true auf den Stack legen
FALSE false auf den Stack legen
LOAD i i (Index/Adresse) Wert aus Speicherzelle i laden (auf den Stack)
STORE i i (Index/Adresse) obersten Stack-Wert in Speicherzelle i speichern
JUMP i i (Ziel) unbedingter Sprung zu Instruktion/Label i
FJUMP i i (Ziel) „False-Jump“: springt zu i, wenn Bedingung `false t
READ Eingabe lesen (Wert auf den Stack)
WRITE obersten Stack-Wert ausgeben
ALLOC i i (Anzahl) Speicher für i Variablen/Speicherzellen reservieren
HALT Programm beenden

Übersetzung (High-Level → Bytecode)

Komplexere Konstrukte (z.B. Ausdrücke, if, while) werden in eine Sequenz solcher einfachen Instruktionen übersetzt; Sprünge sind dabei zentral für Kontrollstrukturen.

Bezug zu Funktionen

Funktionsaufrufe werden im Bytecode/VM-Kontext typischerweise über Stacks und eine passende Laufzeitorganisation (z.B. Aktivierungsdaten pro Aufruf) realisiert.

EBNF

Die Idee

EBNF ist eine formale Notation, um die Syntax einer Sprachepräzise zu beschreiben (welche Zeichenketten/Programme wohlgeformt sind).

Grundbausteine

Regeln (Produktionen)

Wichtige EBNF-Konstrukte (Operatoren)

Ableitung / Sprache einer Grammatik

Mini-Beispiel


  1. wobei das laut meinem actual SWE Freund mit 10 Jahren Industrieerfahrung Giga Anti-Pattern ist lol ↩︎

  2. datentypen können weggelassen werden, (aber nur wenn man es mit allen macht), dann wird inferred ↩︎

Powered by Forestry.md