# 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+2` → `3`), 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:** açı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` → AST temiz ama dolaylı/karmaşık. ### Karar ✅ **(b) İki ara taban eklenir:** - `ExpressionNode : ASTNode` → `resolvedType`, `isConstant`, `foldedValue`. - `StatementNode : ASTNode` → `isReachable` (ö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 (``) | ❌ 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 |