Przejdź do głównej treści
Grafika przedstawia ukryty obrazek

Budowa prostego interpretera języka skryptowego w C (GCC)

Zdjecie zwiazane z Budowa prostego interpretera jzyka skryptowego w C GCC

Wprowadzenie

W tym artykule przedstawiono implementację prostego interpretera języka skryptowego napisanego w języku C. Projekt pokazuje klasyczną architekturę:

kod źródłowy → lexer → parser → AST → interpreter

Celem jest zrozumienie fundamentów działania języków programowania bez użycia zewnętrznych narzędzi.

1. Założenia języka

Zaimplementowany język obsługuje:

  • zmienne (let)
  • operacje arytmetyczne (+ - * /)
  • porównania (< > ==)
  • instrukcję print
  • instrukcje sterujące:
    • if
    • while
  • bloki {}

Przykład programu

let x = 0;

while x < 5 {
    print x;
    let x = x + 1;
}

2. Analiza leksykalna (Lexer)

Lexer przekształca tekst wejściowy w strumień tokenów.

Typy tokenów

TOKEN_NUMBER, TOKEN_IDENT,
TOKEN_LET, TOKEN_PRINT,
TOKEN_IF, TOKEN_WHILE,
...

Kluczowe elementy

Pomijanie białych znaków

while (isspace(src[pos])) pos++;

Rozpoznawanie liczb

while (isdigit(src[pos]))

Rozpoznawanie identyfikatorów i słów kluczowych

if (!strcmp(t.value, "let")) t.type = TOKEN_LET;

Operatory i symbole

Obsługiwane są m.in.:

  • + - * /
  • ==
  • { } ( )

3. Parser i AST

Parser buduje drzewo składniowe (AST).

Typy węzłów

NODE_NUMBER
NODE_VAR
NODE_BINARY
NODE_ASSIGN
NODE_PRINT
NODE_BLOCK
NODE_IF
NODE_WHILE

4. Parsowanie wyrażeń

Parser implementuje priorytety operatorów:

expression → comparison
comparison → arithmetic ( < > == )
arithmetic → term ( + - )
term → factor ( * / )
factor → liczba | zmienna | (wyrażenie)

Dzięki temu:

2 + 3 * 4 = 14

5. Parsowanie instrukcji

Obsługiwane konstrukcje:

Deklaracja zmiennej

let x = 5;

Wypisanie

print x;

Warunek

if x < 10 {
    print x;
}

Pętla

while x < 10 {
    ...
}

6. Struktura AST

AST jest reprezentowane jako struktura z unią:

struct AST {
    NodeType type;

    union {
        int number;
        char name[64];
        ...
    };
};

To pozwala na przechowywanie różnych typów danych w jednym węźle.

7. Interpreter

Interpreter wykonuje AST.

Przechowywanie zmiennych

Lista jednokierunkowa:

typedef struct Var {
    char name[64];
    int value;
    struct Var* next;
} Var;

Operacje na zmiennych

Ustawienie:

set_var(name, value)

Odczyt:

get_var(name)

8. Ewaluacja wyrażeń

Funkcja eval() oblicza wartość węzłów:

case NODE_BINARY:
    int l = eval(left);
    int r = eval(right);

Obsługiwane operatory:

  • + - * /
  • < >
  • ==

9. Wykonywanie instrukcji

Funkcja exec():

Przypisanie

set_var(...)

Print

printf("%d\n", ...)

Blok

for (...) exec(...)

If

if (condition) ...

While

while (condition) ...

10. Scope (ważna decyzja projektowa)

Bloki {} nie tworzą nowego scope.

Konsekwencje:

prostsza implementacja
brak shadowingu zmiennych
łatwiejsze debugowanie

Przykład:

let x = 0;

while x < 3 {
    let x = x + 1;  // nadpisuje x
}

11. Wczytywanie pliku

Program przyjmuje plik jako argument:

./minilang program.txt

Implementacja:

fopen → fread → buffer

12. Architektura programu

lexer      → tokeny
parser     → AST
interpreter→ wykonanie

Każda warstwa jest wyraźnie oddzielona.

13. Ograniczenia

Aktualna wersja:

  • brak else
  • brak funkcji
  • brak typów innych niż int
  • brak zarządzania pamięcią (brak free)

14. Możliwe rozszerzenia

średni poziom

  • else
  • funkcje (fn)
  • scope lokalny

zaawansowane

  • VM (bytecode)
  • garbage collector
  • stringi i tablice

15. Wnioski

Projekt pokazuje:

  • jak działa parser rekurencyjny
  • jak buduje się AST
  • jak interpreter wykonuje kod
  • jak działa prosty język programowania

Jest to solidna baza do budowy bardziej zaawansowanych systemów.

16. Gotowy program

Kod źródłowy minilang.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>

/* ================= TOKENS ================= */

typedef enum {
    TOKEN_NUMBER, TOKEN_IDENT,
    TOKEN_LET, TOKEN_PRINT,
    TOKEN_IF, TOKEN_WHILE,

    TOKEN_PLUS, TOKEN_MINUS, TOKEN_STAR, TOKEN_SLASH,
    TOKEN_EQ, TOKEN_EQEQ,
    TOKEN_LT, TOKEN_GT,

    TOKEN_LPAREN, TOKEN_RPAREN,
    TOKEN_LBRACE, TOKEN_RBRACE,
    TOKEN_SEMI,

    TOKEN_EOF
} TokenType;

typedef struct {
    TokenType type;
    char value[64];
} Token;

/* ================= GLOBAL ================= */

char* src;
int pos = 0;
Token current_token;

/* ================= LEXER ================= */

void skip_ws() {
    while (isspace(src[pos])) pos++;
}

int is_alpha(char c) {
    return isalpha(c) || c == '_';
}

Token next_token() {
    skip_ws();

    Token t;
    t.value[0] = '\0';

    char c = src[pos];

    if (c == '\0') {
        t.type = TOKEN_EOF;
        return t;
    }

    if (isdigit(c)) {
        int i = 0;
        while (isdigit(src[pos]))
            t.value[i++] = src[pos++];
        t.value[i] = '\0';
        t.type = TOKEN_NUMBER;
        return t;
    }

    if (is_alpha(c)) {
        int i = 0;
        while (is_alpha(src[pos]))
            t.value[i++] = src[pos++];
        t.value[i] = '\0';

        if (!strcmp(t.value, "let")) t.type = TOKEN_LET;
        else if (!strcmp(t.value, "print")) t.type = TOKEN_PRINT;
        else if (!strcmp(t.value, "if")) t.type = TOKEN_IF;
        else if (!strcmp(t.value, "while")) t.type = TOKEN_WHILE;
        else t.type = TOKEN_IDENT;

        return t;
    }

    pos++;

    switch (c) {
        case '+': t.type = TOKEN_PLUS; break;
        case '-': t.type = TOKEN_MINUS; break;
        case '*': t.type = TOKEN_STAR; break;
        case '/': t.type = TOKEN_SLASH; break;
        case '<': t.type = TOKEN_LT; break;
        case '>': t.type = TOKEN_GT; break;

        case '=':
            if (src[pos] == '=') {
                pos++;
                t.type = TOKEN_EQEQ;
            } else t.type = TOKEN_EQ;
            break;

        case '(': t.type = TOKEN_LPAREN; break;
        case ')': t.type = TOKEN_RPAREN; break;
        case '{': t.type = TOKEN_LBRACE; break;
        case '}': t.type = TOKEN_RBRACE; break;
        case ';': t.type = TOKEN_SEMI; break;

        default:
            printf("Unknown char: %c\n", c);
            exit(1);
    }

    return t;
}

void advance() {
    current_token = next_token();
}

/* ================= AST ================= */

typedef enum {
    NODE_NUMBER, NODE_VAR, NODE_BINARY,
    NODE_ASSIGN, NODE_PRINT,
    NODE_BLOCK, NODE_IF, NODE_WHILE
} NodeType;

typedef struct AST AST;

struct AST {
    NodeType type;

    union {
        int number;
        char name[64];

        struct {
            AST* left;
            AST* right;
            char op;
        } binary;

        struct {
            char name[64];
            AST* value;
        } assign;

        struct {
            AST* expr;
        } print;

        struct {
            AST** stmts;
            int count;
        } block;

        struct {
            AST* cond;
            AST* then_branch;
        } if_stmt;

        struct {
            AST* cond;
            AST* body;
        } while_stmt;
    };
};

/* ================= PARSER ================= */

AST* new_node(NodeType t) {
    AST* n = malloc(sizeof(AST));
    n->type = t;
    return n;
}

/* ---- expressions ---- */

AST* parse_expression();

AST* parse_factor() {
    if (current_token.type == TOKEN_NUMBER) {
        AST* n = new_node(NODE_NUMBER);
        n->number = atoi(current_token.value);
        advance();
        return n;
    }

    if (current_token.type == TOKEN_IDENT) {
        AST* n = new_node(NODE_VAR);
        strcpy(n->name, current_token.value);
        advance();
        return n;
    }

    if (current_token.type == TOKEN_LPAREN) {
        advance();
        AST* n = parse_expression();
        if (current_token.type != TOKEN_RPAREN) {
            printf("Expected )\n");
            exit(1);
        }
        advance();
        return n;
    }

    printf("Unexpected token\n");
    exit(1);
}

AST* parse_term() {
    AST* node = parse_factor();

    while (current_token.type == TOKEN_STAR ||
           current_token.type == TOKEN_SLASH) {

        char op = (current_token.type == TOKEN_STAR) ? '*' : '/';
        advance();

        AST* right = parse_factor();

        AST* n = new_node(NODE_BINARY);
        n->binary.left = node;
        n->binary.right = right;
        n->binary.op = op;
        node = n;
    }

    return node;
}

AST* parse_arith() {
    AST* node = parse_term();

    while (current_token.type == TOKEN_PLUS ||
           current_token.type == TOKEN_MINUS) {

        char op = (current_token.type == TOKEN_PLUS) ? '+' : '-';
        advance();

        AST* right = parse_term();

        AST* n = new_node(NODE_BINARY);
        n->binary.left = node;
        n->binary.right = right;
        n->binary.op = op;
        node = n;
    }

    return node;
}

AST* parse_expression() {
    AST* node = parse_arith();

    if (current_token.type == TOKEN_LT ||
        current_token.type == TOKEN_GT ||
        current_token.type == TOKEN_EQEQ) {

        char op;
        if (current_token.type == TOKEN_LT) op = '<';
        else if (current_token.type == TOKEN_GT) op = '>';
        else op = '=';

        advance();
        AST* right = parse_arith();

        AST* n = new_node(NODE_BINARY);
        n->binary.left = node;
        n->binary.right = right;
        n->binary.op = op;
        return n;
    }

    return node;
}

/* ---- statements ---- */

AST* parse_statement();

AST* parse_block() {
    AST* block = new_node(NODE_BLOCK);
    block->block.stmts = NULL;
    block->block.count = 0;

    if (current_token.type != TOKEN_LBRACE) {
        printf("Expected {\n");
        exit(1);
    }
    advance();

    while (current_token.type != TOKEN_RBRACE) {
        block->block.stmts = realloc(block->block.stmts,
            sizeof(AST*) * (block->block.count + 1));
        block->block.stmts[block->block.count++] = parse_statement();
    }

    advance();
    return block;
}

AST* parse_statement() {
    if (current_token.type == TOKEN_LET) {
        advance();

        AST* n = new_node(NODE_ASSIGN);
        strcpy(n->assign.name, current_token.value);
        advance();

        advance(); // =

        n->assign.value = parse_expression();

        advance(); // ;
        return n;
    }

    if (current_token.type == TOKEN_PRINT) {
        advance();

        AST* n = new_node(NODE_PRINT);
        n->print.expr = parse_expression();

        advance(); // ;
        return n;
    }

    if (current_token.type == TOKEN_IF) {
        advance();

        AST* n = new_node(NODE_IF);

        n->if_stmt.cond = parse_expression();
        n->if_stmt.then_branch = parse_block();

        return n;
    }

    if (current_token.type == TOKEN_WHILE) {
        advance();

        AST* n = new_node(NODE_WHILE);

        n->while_stmt.cond = parse_expression();
        n->while_stmt.body = parse_block();

        return n;
    }

    printf("Unknown statement\n");
    exit(1);
}

/* ================= INTERPRETER ================= */

typedef struct Var {
    char name[64];
    int value;
    struct Var* next;
} Var;

Var* vars = NULL;

void set_var(const char* name, int val) {
    Var* v = vars;
    while (v) {
        if (!strcmp(v->name, name)) {
            v->value = val;
            return;
        }
        v = v->next;
    }

    Var* n = malloc(sizeof(Var));
    strcpy(n->name, name);
    n->value = val;
    n->next = vars;
    vars = n;
}

int get_var(const char* name) {
    Var* v = vars;
    while (v) {
        if (!strcmp(v->name, name))
            return v->value;
        v = v->next;
    }
    printf("Undefined: %s\n", name);
    exit(1);
}

int eval(AST* n);

int eval(AST* n) {
    switch (n->type) {
        case NODE_NUMBER: return n->number;
        case NODE_VAR: return get_var(n->name);

        case NODE_BINARY: {
            int l = eval(n->binary.left);
            int r = eval(n->binary.right);

            switch (n->binary.op) {
                case '+': return l + r;
                case '-': return l - r;
                case '*': return l * r;
                case '/': return l / r;
                case '<': return l < r;
                case '>': return l > r;
                case '=': return l == r;
            }
        }
        default: return 0;
    }
}

void exec(AST* n);

void exec(AST* n) {
    switch (n->type) {
        case NODE_ASSIGN:
            set_var(n->assign.name, eval(n->assign.value));
            break;

        case NODE_PRINT:
            printf("%d\n", eval(n->print.expr));
            break;

        case NODE_BLOCK:
            for (int i = 0; i < n->block.count; i++)
                exec(n->block.stmts[i]);
            break;

        case NODE_IF:
            if (eval(n->if_stmt.cond))
                exec(n->if_stmt.then_branch);
            break;

        case NODE_WHILE:
            while (eval(n->while_stmt.cond))
                exec(n->while_stmt.body);
            break;

        default:
            break;
    }
}

/* ================= FILE ================= */

char* read_file(const char* path) {
    FILE* f = fopen(path, "rb");
    if (!f) {
        printf("Cannot open file\n");
        exit(1);
    }

    fseek(f, 0, SEEK_END);
    long size = ftell(f);
    rewind(f);

    char* buffer = malloc(size + 1);
    fread(buffer, 1, size, f);
    buffer[size] = '\0';

    fclose(f);
    return buffer;
}

/* ================= MAIN ================= */

int main(int argc, char** argv) {
    if (argc < 2) {
        printf("Usage: %s program.txt\n", argv[0]);
        return 1;
    }

    src = read_file(argv[1]);
    advance();

    while (current_token.type != TOKEN_EOF) {
        AST* stmt = parse_statement();
        exec(stmt);
    }

    return 0;
}

Przykładowy skrypt test.ml

let x = 0;

while x < 5 {
    print x;
    let x = x + 1;
}

if x == 5 {
    print 999;
}

Kompilacja i uruchomienie

gcc -o minilang minilang.c
sudo chmod +x ./minilang
./minilang test.ml

Wynik działania skryptu

0
1
2
3
4
999

Podsumowanie

Zaimplementowany interpreter to:

pełny pipeline kompilacji (bez kompilacji do kodu maszynowego)
działający język skryptowy
fundament pod własny język programowania

Najważniejsza wartość: zrozumienie, jak działają języki „od środka”.

1 kwietnia 2026 1

Kategorie

programowanie

Tagi

c gcc

Dziękujemy!
()

Informacja o cookies

Moja strona internetowa wykorzystuje wyłącznie niezbędne pliki cookies, które są wymagane do jej prawidłowego działania. Nie używam ciasteczek w celach marketingowych ani analitycznych. Korzystając z mojej strony, wyrażasz zgodę na stosowanie tych plików. Możesz dowiedzieć się więcej w mojej polityce prywatności.