Sesja 9 — Rejestry: więcej niż jeden akumulator

✅ Sprawdź się z sesji 8

Cel

Zastąpić pojedynczy akumulator z poprzednich sesji tablicą kilku “rejestrów” — dokładnie tak, jak robi to prawdziwy procesor (i CHIP-8, z jego 16 rejestrami V).

Teoria w pigułce

Ograniczenie jednego akumulatora

Maszyna z sesji 5-8 ma tylko jedno miejsce na wynik — akumulator. Nie da się nią policzyć np. (a + b) * (c - d) bez zgubienia jednej z wartości pośrednich. Prawdziwe procesory mają wiele rejestrów właśnie po to, żeby trzymać kilka wartości naraz.

Tablica jako zestaw rejestrów

int rejestry[4] = {0, 0, 0, 0};   // R0, R1, R2, R3

rejestry[0] = 10;
rejestry[1] = 5;
rejestry[2] = rejestry[0] + rejestry[1];   // R2 = R0 + R1

Zauważ: to nie jest nowa koncepcja C — to ta sama tablica, którą znacie od sesji 5, tylko interpretowana inaczej: każdy indeks to teraz osobne miejsce na wynik, a nie kolejna instrukcja programu.

Instrukcja z dwoma operandami: numer rejestru

Żeby instrukcja mogła powiedzieć “dodaj do rejestru R2”, sama instrukcja musi przechowywać numer rejestru docelowego, nie tylko wartość:

struct Instrukcja {
    enum OpCode kod;
    int rejestr_docelowy;   // ktory rejestr modyfikujemy
    int wartosc;             // literal, albo numer innego rejestru -- zalezy od operacji
};
NoteTo jest dosłownie V[x] z CHIP-8

6XKK (LD Vx, byte) w CHIP-8 to dokładnie ten wzorzec: “X” mówi, KTÓRY z 16 rejestrów V ma dostać wartość “KK”. Struktura, którą dziś zbudujecie, ma trzy pola z dokładnie tego samego powodu, dla którego opcode CHIP-8 koduje w sobie zarówno typ operacji, jak i numer rejestru.

Zadania

Zadanie 1 — rozbuduj maszynę o rejestry

Zmień struct Instrukcja, dodając pole int rejestr_docelowy. Zmień execute_instruction, żeby przyjmowała int rejestry[] (całą tablicę) zamiast pojedynczego int akumulator, i modyfikowała rejestry[instr.rejestr_docelowy] zamiast zwracać nową wartość akumulatora:

void execute_instruction(int rejestry[], struct Instrukcja instr) {
    switch (instr.kod) {
        case OP_LOAD:
            rejestry[instr.rejestr_docelowy] = instr.wartosc;
            break;
        case OP_ADD:
            rejestry[instr.rejestr_docelowy] += instr.wartosc;
            break;
        // ...
    }
}

Zwróć uwagę: funkcja jest teraz void, nie int — nie zwraca wartości, tylko modyfikuje tablicę, która została jej przekazana. To działa (w odróżnieniu od przykładu z zwieksz(int x) z sesji 6!), bo tablice w C zachowują się inaczej niż pojedyncze zmienne przy przekazywaniu do funkcji — do tego wrócimy dokładnie w sesji 11.

Zadanie 2 — operacja między dwoma rejestrami

Dodaj OP_ADD_REG — dodaje wartość jednego rejestru do drugiego (nie literał). Dla tej operacji pole wartosc przechowuje indeks rejestru źródłowego. Nie dodajemy czwartego pola: znaczenie argumentu zależy od kodu operacji. Przed dostępem sprawdź zakres obu indeksów.

case OP_ADD_REG:
    rejestry[instr.rejestr_docelowy] += rejestry[instr.wartosc];
    break;

Zadanie 3 — policz coś, co NAPRAWDĘ wymaga kilku rejestrów

Uwaga: nie każde wyrażenie z nawiasami tego wymaga — (3 + 4) * 2 policzycie jednym akumulatorem bez problemu (LOAD 3, ADD 4, MUL 2 — żadna wartość pośrednia nie ginie, bo liczymy liniowo, krok po kroku). Rejestrów potrzeba dopiero, gdy musicie jednocześnie pamiętać dwie niezależnie policzone wartości, zanim je złączycie — np. (3 + 4) + (10 + 20): żeby dodać do siebie wynik 3+4 i wynik 10+20, musicie mieć oba w pamięci naraz (jeden akumulator nadpisałby pierwszy wynik, licząc drugi).

Napisz “program” (tablicę struct Instrukcja) liczący (3 + 4) + (10 + 20): policz pierwszą sumę w R0, drugą w R1, potem OP_ADD_REG złącz je w jednym z rejestrów. Wypisz zawartość rejestrów na końcu (przez OP_PRINT albo wypisz_rejestry).

Kryterium sukcesu

  • Maszyna operuje na tablicy kilku rejestrów zamiast jednego akumulatora.
  • OP_ADD_REG poprawnie dodaje wartość jednego rejestru do drugiego.
  • Program testowy z zadania 3 daje poprawny wynik (37), korzystając z więcej niż jednego rejestru po drodze.