saqut-compiler/docs/adr-frontend-analiz.md

17 KiB
Raw Blame History

saQut Derleyici — Frontend & Semantic Analiz Karar Kaydı (ADR-006 …)

Bu belge, docs/fikirler.md'deki ADR-001…005'in devamıdır. Orada backend stratejisi, parser mimarisi, header-only tercihi, token sistemi ve IR tasarımı kararlaştırılmıştı. Bu belge ise frontend'in tamamlanması — symbol table, semantic analiz ve optimizasyon framework'ü — etrafında alınan kararları, neden alındıklarını, elenen alternatifleri ve gelecekteki sonuçlarını kaydeder.

Bu kararlar bir tasarım oturumunda (kullanıcı + asistan) tartışılarak alındı. Tartışmanın tam akışı için bkz. docs/transkript-frontend-tasarim.md. Uygulama planı için bkz. docs/roadmap-frontend.md.


ADR-006: Çok-Aşamalı (Multi-Pass) Frontend Mimarisi

Bağlam

Derleyici tek bir monolitik geçişle çalışmaz; lexing, parsing, analiz, optimizasyon, kod üretimi gibi birbirinden bağımsız aşamalardan oluşur. saQut'un "alet çantası" (toolbox) felsefesi gereği bu aşamaların her biri:

  • bağımsız çalışabilmeli,
  • net bir girdi/çıktı sözleşmesine sahip olmalı,
  • gerektiğinde çoğaltılabilmeli (projenin amacına sadık kalarak).

CLI komutları (tokens, ast, symbols, …) zaten bu aşama-modülü yapısının dışa vurumudur — her komut bir aşamanın çıktısını gösterir.

Değerlendirilen Yaklaşımlar

Tek geçişli (single-pass) parser+analiz

  • + Basit, hızlı.
  • Forward reference (ileri başvuru) imkânsızlaşır; her şey tanımdan önce bilinmek zorunda kalır.
  • Analiz ve syntax iç içe girer, test edilemez, bakımı zor.

Çok geçişli (multi-pass) — net aşamalar

  • + Her aşama tek bir iş yapar, ayrı ayrı test edilir.
  • + Forward reference mümkün olur.
  • + Aşamalar incelenebilir (saqut ast, saqut symbols …).
  • Daha fazla kod ve veri yapısı; aşamalar arası sözleşme tasarımı gerekir.

Karar

Çok-aşamalı frontend. Aşamalar şu üç katmana ayrılır (klasik derleyici mimarisi):

FRONTEND                 MIDDLE-END               BACKEND
lexer → token →          optimizasyon             IR lowering →
parser → AST →           (opsiyonel, iteratif,    kod üretimi
symbol table →           toggle'lı, ortak         (C transpile /
semantic analiz          gösterim üstünde)        QBE / JIT)
(annotated AST)
  • Pass 1 (Syntax): token → ham AST. (Büyük ölçüde mevcut.)
  • Pass 2 (Symbol): AST → SymbolTable (scope'lu, iki-geçişli — bkz. ADR-011).
  • Pass 3 (Semantic / "ASTyi derinleştir"): symbol table + AST kullanılarak her node zenginleştirilir (tip, symbol bağı, erişilebilirlik, reference sayısı).

"Parser ve symbol hikayesini bitirmek" = frontend'i bitirmek. Optimizasyon ayrı bir katmandır (middle-end), backend'ler bu ortak çıktıdan beslenir.

Neden bu katmanlama? Birden çok backend (C transpile, QBE, JIT) planlandığı için, ortak işler (analiz, optimizasyon) bir kez ortak katmanda yapılmalı; yoksa her backend aynı optimizasyonu yeniden yazar.


ADR-007: Analiz (Annotation) ile Optimizasyon (Transformation) Ayrımı

Bağlam

docs/fikirler.md ve docs/todo.md'nin temel prensibi: "AST bellek canavarı. Hiçbir bilgi atılmaz." Aynı zamanda constant folding (1+23), dead code elimination gibi optimizasyonlar isteniyor. Bu ikisi doğrudan çelişir gibi görünür: optimizasyon AST'yi bozarsa, kaynak kodun izdüşümü kaybolur ve saqut ast artık kullanıcının yazdığını değil, optimize edilmiş hali gösterir.

Ayrıca kritik bir kullanıcı gereksinimi belirlendi: kullanıcı, AST'nin veya sembol tablosunun optimizasyondan önceki ve sonraki halini ayrı ayrı görebilmeli.

İki Kavram

  • Analiz (annotation) = programın gerçekleri. "Bu node sabit, değeri 3", "bu kod erişilemez", "bu ifadenin tipi int", "bu değişken 2 kez kullanıldı". Bunlar değişiklik değil, tespittir. Backend'den bağımsızdır.
  • Optimizasyon (transformation) = ağacı/IR'ı gerçekten değiştirmek. 1+2'yi 3 ile değiştirmek, ölü kodu silmek.

Karar

İki kavram net ayrılır:

  1. Analiz, orijinal AST'nin üstüne yerinde işaretleme yapar (node'lara tip, symbol bağı, erişilebilirlik, constness ekler). Ağacı bozmaz, zenginleştirir. Orijinal AST hâlâ kaynak kodun tam izdüşümüdür.

  2. Optimizasyon dönüşümü, ağacın bir KOPYASI (klon) üzerinde yapılır. Orijinal analizli AST = "öncesi"; klon + dönüştürülmüş AST = "sonrası". Ağaç klonlamak ucuz ve basittir, yalnızca --optimized istendiğinde yapılır.

Sonuç: Hem "bellek canavarı" felsefesi korunur (orijinal AST her şeyi tutar), hem optimizasyon yapılır, hem de öncesi/sonrası ayrı ayrı incelenebilir.

saqut ast file.sqt              → ham + annotate edilmiş AST (1+2 burada durur)
saqut ast file.sqt --optimized  → klon, folding uygulanmış (3 var)

ADR-008: Optimizasyon Konumu — AST mı, IR mı?

Bağlam

"Optimizasyonu IR/derleme zamanında mı yapmalıyız, yoksa AST aşamasında mı?" sorusu tartışıldı. İki uç yaklaşım var:

  • AST seviyesi: kaynak-seviyesi, dile yakın, incelenebilir.
  • IR seviyesi:ık kontrol-akış grafiği (CFG), dataflow analizi için uygun (LLVM modeli).

Karar

Hibrit, optimizasyon türüne göre bölünür:

  • Kaynak-seviyesi, ağaç-yerel optimizasyonlar (constant folding, ölü kod işaretleme, unused variable) → AST'de yapılır. Çünkü:

    1. Dil JS gibi basit; ağır optimizasyona ihtiyaç yok.
    2. Backend-bağımsız → C transpile, QBE, JIT üçü birden faydalanır.
    3. İncelenebilir kalır (saqut ast --optimized) — projenin varlık sebebi.
  • CFG/dataflow gerektiren optimizasyonlar ("bir kez atanıp bir kez kullanılan değişken" = copy propagation, common subexpression elimination, loop optimizasyonları) → IR'de yapılır, IR olgunlaşınca ertelenir. Çünkü bunlar açık kontrol akışı ister, ağaçta yapmak işkencedir.

Neden backend'e bırakmıyoruz? 3 backend varsa, optimizasyon backend'e konulursa 3 kez yazılır. Ortak katmanda (middle-end) bir kez yazılır.


ADR-009: Optimizasyon Pass Yönetimi — Sabit Sayı Değil, Fixpoint

Bağlam

Optimizasyon adımları birbirini tetikler: constant folding yeni ölü kod doğurur, dead code elimination yeni kullanılmayan değişken doğurur. Tek geçişte zincirleme fırsatlar kaçırılır. "5 pass mı, 10 pass mı çalıştıralım?" sorusu yanlış kurgu.

Not: Buradaki "pass", ADR-006'daki derleyici aşamalarından (lexing/parsing gibi makro-aşamalar) farklıdır. Burada "pass" = tek bir optimizasyon adımının AST üzerindeki bir gezisidir.

Karar

Fixpoint döngüsü. Önceden belirlenmiş sayıda değil; bir pass havuzu, hiçbir pass değişiklik yapmayana kadar döngüde çalışır. Belki 2 tur sürer, belki 7 — kodun kendisi belirler.

  • Her tur bir öncekinden daha az iş yapar (giderek azalan değişiklik), ta ki sıfır değişiklikle stabilize olana kadar.
  • Pass'ler CompilerConfig ile tek tek açılıp kapatılabilir.
  • OptimizationManager pass listesini tutar, sırayı ve fixpoint döngüsünü yönetir.

Düzeltme notu: "Her pass bir öncekinden daha kolay" sezgisi yanlıştır. Doğrusu: her tur daha az değişiklik yapar. Analiz pass'leri (symbol table, type check) "kolaylaşmaz"; onlar bir kez çalışır.


ADR-010: Tip Sistemi Tasarımı

Bağlam

Dil tipli olacak. Şu anda varType/returnType AST'de yalnızca std::string. Tip kontrolünü string karşılaştırmasıyla yazmak kırılgandır ve int[], struct Point, fonksiyon tipi gelince baştan yazmayı gerektirir.

Alınan Kararlar

Minimal ama genişletilebilir Type sınıfı (src/core/type.hpp):

  • kind: Primitive / Array / Struct / Function / Error.
  • Primitifler: int, float, double, char, string, bool, void.
  • Array → eleman tipi (boyut tipin parçası DEĞİL — bkz. aşağı).
  • Function → dönüş tipi + parametre tipleri.
  • İleride Pointer, Generic eklenebilir.

Error tipi şart. Tip hatası olduğunda node'a Error atanır; böylece ardışık sahte hatalar üretilmez (tek hata, tek mesaj).

Gizli (implicit) dönüşüm YOK. int → float otomatik olmaz; her şey açık.

  • Tek istisna: sabit ifadelerde (constant folding) — int a = 5 / 2;2. Sabitler üzerinde küçük analiz/hesap yapılır.

Tip çıkarımı (auto/var) YOK. Her şey açıkça tiplenir. auto keyword'ü yok sayılır. Sebep: basitlik, öngörülebilirlik, kafa karışıklığını önlemek.

Array tip temsili: int[] (boyut tipte yok). int[] sadece "int dizisi"; boyut tip eşitliğine girmez (JS gibi). Tip kontrolü basit kalır.

Neden genişletilebilir? "Bu dilin geleceğini bilmiyoruz; beklenenden popüler de olabilir, yıllarca repolarda tozlanabilir de." Temel sağlam ve büyümeye açık olmalı.


ADR-011: Scope ve Forward Reference Kuralları

Bağlam

Dil "Java gibi forward reference, C gibi syntax, başta OOP yok" olarak tasarlandı (JS yalnızca syntax basitliği örneği olarak verildi; JS'in kötü yanları — null/undefined ikiliği, var hoisting — alınmıyor).

"Hoisting" nedir?

Bir tanımın, yazıldığı satırdan önce de görünür olması (scope'un tepesine "kaldırılmış" gibi).

Karar

Asimetrik kurallar (tam olarak Java'nın davranışı):

  • Üst seviye (global): tam forward reference (hoisting var). Fonksiyonlar, global değişkenler, struct'lar sırasından bağımsız her yerde görünür.

    int main() { return kare(5); }   // kare aşağıda ama görünür → OK
    int kare(int n) { return n * n; }
    

    Neden güvenli? main'in gövdesi tanımlandığı anda çalışmaz; çağrılınca çalışır, o ana kadar kare zaten vardır. Tanımların çalışma sırası yoktur.

  • Lokal (fonksiyon içi): declare-before-use (hoisting YOK).

    int main() {
        int x = y + 1;   // HATA: y henüz tanımlı değil
        int y = 5;
    }
    

    Neden? Lokal değişkenin bir çalışma sırası ve değeri vardır; tanımdan önce kullanmak, var olmayan/değeri olmayan bir şeyi kullanmaktır. Local hoisting olsaydı isim olur ama değeri çöp/undefined olurdu (JS var derdi) — kaçınılan durum.

Asimetri tutarsızlık değildir: global tanımlar yerinde çalışmaz (forward ref güvenli), lokal değişkenlerin sırası ve değeri vardır (declare-before-use güvenli). Bu, Java/C#'ın da davranışıdır.

Duplicate kesinlikle yasak. Aynı scope'ta aynı isimli iki değişken/fonksiyon tanımlanamaz → diagnostic. (Overloading yok.)

Shadowing serbest. İç scope, dış scope'u gölgeleyebilir (hata değil).

Scope oluşturan node'lar: Program (global), FunctionDecl (parametreler), Block, for/while (init değişkeni döngüye ait; döngü dışında görünmez). Her katman bir namespace tutar; değişken bulunamazsa bir üst katmanda aranır.

Symbol Table'ın İki Geçişi

Sadece üst seviyede iki geçiş gerekir:

  • Geçiş 1: tüm üst-seviye tanımları (fonksiyon imzaları, struct isim+alanları, global değişkenler) global scope'a hoist et.
  • Geçiş 2: gövdelere in; lokal'leri declare-before-use ile topla, her Identifier'ı çöz, reference ekle.
  • Fonksiyon içi tek geçiş yeter (lokal'de forward ref yok). "Öncesi/sonrası" derdi yalnızca global'ler içindir, onu da Geçiş 1 çözer (global'ler en baştan tamamen doludur).

ADR-012: ExpressionNode / StatementNode Ara Tabanları

Bağlam

Şu anda tüm AST node'ları doğrudan ASTNode'dan türüyor; "ifade" (değer üreten) ve "deyim" (iş yapan ama değer olmayan) ayrımı yok. Tipli bir dilde yalnızca ifadelerin tipi vardır: 5 + 3 → int; if (...) {...} → tipi yok.

resolvedType alanını nereye koyacağımız bir tasarım kararı. Seçenekler:

  • (a) ASTNode tabanına koy → her node'da olur, if/while'da boşa durur.
  • (b) ExpressionNode/StatementNode ara tabanları → alanlar doğru yere oturur.
  • (c) Yan-tablo map<ASTNode*, Annotation> → AST temiz ama dolaylı/karmaşık.

Karar

(b) İki ara taban eklenir:

  • ExpressionNode : ASTNoderesolvedType, isConstant, foldedValue.
  • StatementNode : ASTNodeisReachable (ölü kod analizi için).
ASTNode
 ├─ ExpressionNode   (resolvedType, isConstant, foldedValue)
 │   ├─ LiteralNode / BinaryExpressionNode / IdentifierNode / CallExpressionNode …
 └─ StatementNode    (isReachable)
     ├─ IfStatementNode / WhileStatementNode / ReturnStatementNode / BlockNode …

Kazanımlar:

  1. resolvedType yalnızca tip taşıyabilen node'larda olur.
  2. Parser/analiz "burası ifade olmalı" diyebilir (örn. if koşulu bir ExpressionNode olmalı, fonksiyon argümanı ExpressionNode olmalı).

Önemli: Bu karar, "her şey AST'de" felsefesini bozmaz (bkz. ADR-013); yalnızca analiz alanlarını doğru node sınıflarına dağıtır. Node cpp dosyaları zaten boştu; bu tabanlar onları doldururken ekleniyor. Maliyeti şimdi düşük, sonra yüksek olurdu.


ADR-013: Analiz Verisi Nerede Yaşar — Her Şey AST'de

Bağlam

İki model: (1) her şey AST node'larının üstünde; (2) AST temiz, analiz sonuçları ayrı yan-tablolarda.

  • Her şey AST'de: tek doğruluk kaynağı, gezinmesi kolay (node->type), kullanıcının zihinsel modeli, boş node class'larını doldurur. Ancak öncesi/ sonrası için ağacı klonlamak gerekir.
  • Temiz AST + yan-tablolar: AST sade kalır, çoklu bağımsız analiz mümkün; ancak dolaylılık ve karmaşıklık artar, "node class'larını doldur" isteğine ters.

Karar

Her şey AST node'larının üstünde (kullanıcının modeli):

  • Analiz (tip, constness, erişilebilirlik) = node'lara yerinde işaretlenir.
  • Optimizasyon dönüşümü = ağacın klonunda yapılır (ADR-007), böylece öncesi/sonrası korunur.

Önemli ayrım — "kaç kez kullanıldı" bilgisi node'da değil, Symbol'da:

  • IdentifierNode → işaret ettiği Symbol'a pointer tutar.
  • Symbol → o değişkenin tüm referanslarının listesini + sayısını tutar.
  • ExpressionNode → kendi sonuç tipini, sabit olup olmadığını tutar.

Sebep: kullanım sayısı değişkene aittir, tek bir kullanım node'una değil.

Pointer notu: Burada ve genel olarak derleyici içinde pointer serbestçe kullanılır (Symbol bağları, parent pointer'lar vb.). Kullanıcıya sunulan dilde pointer syntax'ı (*, &) yoktur — bkz. ADR-014.


ADR-014: Dil Kapsamı ve Özellik Kararları

Karar — Başlangıç Dili (v0)

Özellik Karar Not
Pointer (kullanıcı syntax'ı */&) Yok Ama derleyici/runtime içeride pointer'ı sonuna kadar kullanır
Tuple / Generic (<T,U>) Yok
Class / OOP Yok (başta) class keyword'ü yok sayılır
Struct Var struct A { B bVar } olur (B başka yerde tanımlı); recursive define yok
Array int[] Dinamik yönde; runtime bellek modeli ertelendi
Fonksiyonlar Tipli Dönüş + parametre tipleri zorunlu
auto / tip çıkarımı Yok Her şey açık tipli
Gizli int↔float dönüşümü Yok Sadece sabit folding'de istisna

Dinamik Array'in Bellek Yükümlülüğü (Gelecek Notu)

int[] büyüyebilen array = heap + bir yönetim stratejisi gerektirir. Bu gerçek bir yükümlülüktür ama frontend'i bloklamaz ve kolay yolu vardır:

  • Frontend: yalnızca "bu int dizisi" bilgisini ister; bellek modelinden habersiz.
  • C transpile backend (ilk backend): int[] → C'de struct {int* data; size_t len, cap;}, malloc/realloc/free ile yönetilir. Bellek yönetimi C'den hazır gelir.
  • JIT backend: bellek yönetimini kendi yapmaz; minik bir runtime kütüphanesi (array_new, array_push, array_free) olur, JIT bunlara call emit eder.
  • Yönetim stratejisi (ne zaman free): en basiti scope-tabanlı ownership (array'i tutan değişken scope'tan çıkınca free). GC gerekmez. Runtime'a gelince kararlaştırılır.

Kararların Özet Tablosu

ADR Konu Karar
006 Frontend mimarisi Çok-aşamalı; frontend/middle-end/backend katmanları
007 Analiz vs optimizasyon Analiz yerinde işaretler; optimizasyon klonda dönüştürür
008 Optimizasyon konumu Basitler AST'de, dataflow gerektirenler IR'de
009 Pass yönetimi Fixpoint döngüsü, toggle'lı
010 Tip sistemi Minimal+genişletilebilir Type; gizli dönüşüm yok; Error tipi
011 Scope/forward ref Global'de forward ref, lokal'de declare-before-use (Java gibi)
012 Node hiyerarşisi ExpressionNode / StatementNode ara tabanları
013 Analiz verisi yeri Her şey AST'de; ref-count Symbol'da
014 Dil kapsamı Pointer/class/generic yok; struct+array+tipli fonksiyon var