17 KiB
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'yi3ile değiştirmek, ölü kodu silmek.
Karar
✅ İki kavram net ayrılır:
-
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.
-
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
--optimizedistendiğ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ü:
- Dil JS gibi basit; ağır optimizasyona ihtiyaç yok.
- Backend-bağımsız → C transpile, QBE, JIT üçü birden faydalanır.
- İ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
CompilerConfigile tek tek açılıp kapatılabilir. OptimizationManagerpass 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,Genericeklenebilir.
✅ 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 kadarkarezaten 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
varderdi) — 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)
ASTNodetabanına koy → her node'da olur,if/while'da boşa durur. - (b)
ExpressionNode/StatementNodeara 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 : 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:
resolvedTypeyalnızca tip taşıyabilen node'larda olur.- Parser/analiz "burası ifade olmalı" diyebilir (örn.
ifkoşulu birExpressionNodeolmalı, fonksiyon argümanıExpressionNodeolmalı).
Ö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ğiSymbol'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'destruct {int* data; size_t len, cap;},malloc/realloc/freeile 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 |