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

390 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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<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:**
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 |