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

1138 lines
56 KiB
Markdown
Raw Permalink 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`.
>
> ✅ **Uygulama durumu:** Bu belgedeki ADR-006…019 kararlarında tarif edilen
> makine **kodlandı ve çalışıyor.** Sembol tablosu, semantik analiz, tip sistemi,
> diagnostic motoru, optimizasyon (constant folding + DCE), IR üreteci ve bytecode
> VM'in tamamı uygulandı. `examples/fibonacci.sqt` uçtan uca çalışıyor.
> Güncel "çalışıyor / henüz yok" listesi için bkz. `CLAUDE.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, bytecode VM (birincil)
symbol table → toggle'lı, ortak + ileride C transpile
semantic analiz gösterim üstünde) (makine kodu = uzak
(annotated AST) gelecek; ADR-015)
```
- **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 (birincil: IR+bytecode VM, ADR-015;
ileride: C transpile; çok uzak: makine kodu) hedeflendiği 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ü iki yolla yapılabilir:**
a. **`ast` komutu için klon:** orijinal AST dokunulmadan kalır, klon üstünde
pass'ler çalışır. Kullanıcı "öncesi" ve "sonrası" AST'yi ayrı ayrı
görebilir. `OptimizationManager::optimize()` bu yolu kullanır.
b. **Diğer tüm komutlar için yerinde (in-place):** `run --optimized`,
`ir --optimized` vb. tek versiyon üretiyor — orijinali saklamaya gerek yok.
`OptimizationManager::runPassesInPlace()` bu yolu kullanır, klon maliyeti yok.
**Sonuç:** "bellek canavarı" felsefesi `ast` komutunda korunur; diğer komutlar
gereksiz klon maliyeti taşımaz.
```
saqut ast file.sqt → ham + annotate edilmiş AST (1+2 burada durur)
saqut ast file.sqt --optimized → klon, folding uygulanmış (3 var)
saqut run file.sqt --optimized → yerinde optimize → IR → VM (klon yok)
saqut ir file.sqt --optimized → yerinde optimize → IR dump (klon yok)
```
### Güncelleme — Klon ve sembol tablosu paylaşımı
`deepClone` sembol tablosunu yeniden eşlemez (remap etmez) — klondaki
`IdentifierNode::resolvedSymbol` orijinal `Symbol` nesnelerini gösterir. Bu
**güvenlidir**, çünkü:
- `Symbol::references` bir **konum listesi** (`std::vector<SourceLocation>`),
referans sayacı değildir. Klonda bir `IdentifierNode` silindiğinde bu liste
değişmez.
- `IdentifierNode` destructor'ı yoktur; `resolvedSymbol`'e dokunan hiçbir yıkıcı
kodu çalışmaz.
- Klondaki pass'ler Symbol nesnelerini **okur** (slot numarası, tip vb.),
**yazmaz** — paylaşım salt-okunur (read-only) kullanımdır.
**Parent pointer'lar** ise yeniden bağlanır — klon node'larının `parent`'ı
orijinali değil, klonu gösterir (deepClone bunu zaten yapar).
Önceki versiyon "sembol tablosu klonlanır ve remap edilir" diyordu; bu hem hiç
implement edilmedi hem de gerekli değildi. Düzeltildi.
---
## 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 → bytecode VM ve ileride C transpile 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.
### Güncelleme — Sonlanma değişmezi (termination invariant)
Fixpoint döngüsünün **sonlanacağı garanti edilmeli**. İki seçenekten en az biri
zorunludur:
1. **Monotonluk:** havuzdaki tüm pass'ler **monoton** olmalı — yalnızca
küçültür/sadeleştirir, asla büyütmez. Constant folding ve dead code
elimination bugün monotondur, dolayısıyla fixpoint sonlanır.
2. **Sert iterasyon tavanı (cap):** bir üst sınır (örn. `maxFixpointRounds`).
**Neden gerekli:** ileride **büyüten** pass'ler (inlining, loop unrolling)
eklenirse, naif fixpoint **salınabilir** (A büyütür, B küçültür, sonsuz döngü).
Büyüten bir pass eklendiği an, monotonluk bozulur ve **iterasyon tavanı zorunlu
hale gelir.** Bu değişmez şimdiden yazıya geçirildi ki ileride unutulmasın.
### Güncelleme — "Analiz bir kez çalışır" çelişkisinin çözümü
ADR-013 "analiz bir kez çalışır" diyor; ama folding **erişilebilirliği**
(`if(false)`) ve **referans sayımlarını** değiştirir, DCE de tam bunlara
dayanır. Eğer analiz gerçekten yalnızca bir kez çalışırsa, fixpoint'in ikinci
turundaki DCE **bayat (stale) veriyle** çalışır ve zincirleme fırsatları kaçırır.
**Çözüm — iki analiz sınıfını ayır:**
- **Kaynağa-bağlı analiz** (her ifadenin tipi, sembol bağları): kaynak değişmediği
sürece sabittir → **bir kez** çalışır, klona taşınır.
- **Türetilmiş/akışa-bağlı analiz** (erişilebilirlik `isReachable`, referans
sayıları): bir dönüşüm bunları geçersizleştirir → **fixpoint döngüsünün her
turunda, klon üzerinde yeniden hesaplanır.**
Yani "analiz bir kez çalışır" ifadesi yalnızca **kaynağa-bağlı** analiz için
geçerlidir; akışa-bağlı analiz tur başına tazelenir. ADR-013 buna göre okunmalı.
---
## 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ı.
### Güncelleme — Sayısal literal tipleme kuralı
"Gizli dönüşüm yok + tip çıkarımı yok" altında `float x = 1;` ifadesi
**tanımsızdı**. Bu açıkça karara bağlanmalı, çünkü tip denetleyicisini (Faz 3)
doğrudan yönlendirir.
**Değerlendirilen iki kural:**
- **(a) Literal her zaman `int`:** `1` daima `int`'tir. `float x = 1;` bir tip
hatasıdır; `float x = 1.0;` yazmak zorunludur. En katı, en öngörülebilir; ama
rahatsız edici ve "gizli dönüşüm yok" ilkesini literallere kadar gereksiz yere
zorlar.
- **(b) Tamsayı literali bağlama-göre tiplenir (context-typed / polymorphic):**
tipsiz bir tamsayı sabiti, beklenen tip ona **kayıpsız** sığıyorsa o tipe
uyarlanır. `float x = 1;` çalışır (`1` → `1.0`); `int y = 1.5;` ise hata
(kayıp olur).
**Karar:****(b) Bağlama-göre tiplenen tamsayı literalleri.**
- Gerekçe: bu bir **değişken-değer dönüşümü değil, bir derleme-zamanı sabitinin
uygun tipte yorumlanmasıdır** — tam olarak ADR-010'un zaten tanıdığı "sabit
istisnası" (`int a = 5/2 → 2`) ruhuyla aynı kapıya çıkar. Çalışma zamanı
`int` değişkenini `float`'a gizlice çevirmek hâlâ **yasaktır**; istisna
yalnızca **literal/sabit** içindir.
- Kural net: *değişken→değişken* gizli dönüşüm yok; *literal→beklenen tip*
kayıpsızsa serbest. `float x = anInt;` hata; `float x = 1;` serbest.
---
## 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).
### Güncelleme — Global "tam forward reference" çok genişti: üç-parçalı kural
İlk metin "global = her zaman forward-reference güvenli" diyordu; bu **fazla
geniş**. Global bir değişkenin **başlatıcısının (initializer) bir çalışma
sırası vardır** (tıpkı lokaller gibi). Düzeltme — üç ayrı kural:
1. **Global fonksiyonlar / struct'lar → tam hoisting.** Tanım anında çalışmazlar,
sıradan bağımsız her yerde görünür. (Güvenli; değişmedi.)
2. **Global değişken isimleri → hoist edilir.** İsim her yerde görünür.
3. **Global değişken başlatıcıları → değer sırasına tabidir** (lokaller gibi) →
**declare-before-use** VEYA bir **definite-assignment (kesin-atama) analizi**
gerektirir.
**Neden:** `int a = b; int b = 5;` global scope'ta, isim-hoisting'e güvenilirse,
`a`'ya **sessizce çöp değer** verir — kaçınmaya çalıştığımız tam o JS `var`
durumu. Java da aynı sebeple bunu kısıtlar. Karar: global başlatıcılar için de
**declare-before-use** uygulanır (en basit, definite-assignment'a gerek
bırakmaz). Yani isim görünür ama **kendinden önceki** bir global başlatıcıda
kullanılabilir.
### Güncelleme — Döngüsel / karşılıklı-özyinelemeli struct tespiti
Pointer olmadığı için tüm struct iç içeliği **değer (by-value)** ile olur →
herhangi bir kapsama döngüsü sonsuz boyut demektir:
```
struct A { B b } // A, B'yi değer olarak içerir
struct B { A a } // B, A'yı değer olarak içerir → sonsuz boyut
```
Bu **derleme hatası olmak zorunda** ve hata kataloğunda **eksikti**. Eklendi:
**`E010` — özyinelemeli/döngüsel struct tanımı.** Symbol toplama sonrası bir
**topolojik / kapsama-döngüsü kontrolü** çalışır (struct'ları düğüm, "alan
olarak içerir" kenarını çevrim arayan bir DFS ile). Çevrim bulunursa `E010`.
(Karşılaştır: `struct A { B b }` + `struct B { int x }` geçerlidir; yalnızca
**çevrim** yasaktır. Pointer olsaydı çevrim mümkün olurdu — ama pointer yok.)
---
## 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 / kalıtım | ❌ Yok (başta) | `class` keyword'ü yok sayılır |
| Closure | ❌ Yok | Bkz. ADR-019 (bellek bağımlılığı) |
| Struct | ✅ Var | `struct A { B bVar }` olur (B başka yerde tanımlı); **çevrim yasak → `E010`** |
| `interface` | ⏸️ Ertelendi | Reddedilmedi; v0 değil — gerekçe aşağıda + ADR-018 |
| 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/literal folding'de istisna (ADR-010) |
### 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.
- **IR + bytecode VM (ilk çalıştırma modeli, ADR-015):** bellek host (C++)
heap'idir; array'ler host tarafında `std::vector` benzeri bir yapıyla tutulur.
VM, array işlemleri için **host fonksiyonlarına** (FFI seam, ADR-016) çağrı
yapar. v0 için özel allocator gerekmez.
- **C transpile backend (ileride, ikinci backend):** `int[]` → C'de
`struct {int* data; size_t len, cap;}`, `malloc/realloc/free` ile yönetilir.
- **Yönetim stratejisi (ne zaman free):** scope-tabanlı ownership (array'i tutan
değişken scope'tan çıkınca free). GC gerekmez — **neden gerekmediği aşağıda
gerekçelendirildi.**
### Güncelleme — Scope-tabanlı bellek artık GEREKÇELİ (bağımlılığı belgele)
> ⚠️ **İPTAL — bu güncelleme ADR-020 ile geçersiz kılındı.** Bileşik tipler artık
> runtime'da referans (JS/Java/C# modeli); bileşikler scope'tan kaçar, bellek
> erişilebilirliğe bağlı, geri-kazanım stratejisi (#56) gerekecek. Aşağıdaki
> "GC gerekmez" sonucu **artık geçerli değildir** — tarihsel bağlam için bırakıldı.
Önceki kaygı ("scope çıkışında free, aliasing/escape altında bozulur") kilitli
dil kimliğiyle **lehte çözüldü:**
> prosedürel + value semantics + kullanıcı pointer'ı yok + closure yok + kaçan
> referans yok → array/string'ler tanımlandıkları scope'tan **kaçamaz** →
> scope-tabanlı ownership (scope çıkışında free) **gerçekten çalışır, GC
> gerekmez.**
**Bağımlılık açıkça yazılır:** scope-tabanlı bellek, *yalnızca* no-pointer /
value-semantics seçimi sayesinde geçerlidir. `interface` değerleri veya kaçan
referanslar eklenirse bu sorun **yeniden açılır**. (Bu, `interface`'i ertelemenin
ikinci sebebidir — bkz. ADR-018, ADR-019.)
---
## ADR-015: Çalıştırma Modeli — IR + Bytecode VM (Makine-Kodu JIT Kapsam Dışı)
### Bağlam
Daha önceki belge/konuşmalarda çalıştırma için "JIT" terimi geçiyordu. Hangi
çalıştırma modeli? Üç uç var: tree-walker, bytecode VM, gerçek makine-kodu JIT.
### Değerlendirilen Yaklaşımlar
- **Tree-walker (AST'yi doğrudan gez-çalıştır):** en basit, ama **çok yavaş**;
her çalıştırmada ağaç gezilir.
- **Makine-kodu JIT** (register allocation, ABI/çağırma sözleşmeleri,
çalıştırılabilir `mmap` bellek): en hızlı; ama **tek faydası ham hızdır**, ki
burada öncelik değil. Determinizmi ve incelenebilirliği zorlaştırır, devasa
mühendislik yükü getirir.
- **IR + bytecode VM** (kendi IR'imize derle, yorumlayıcı döngü ile çalıştır):
determinizm ve incelenebilirliği **doğrudan** sağlar; tree-walker'dan hızlı;
bellek host heap'iyle kolay.
### Karar
**IR + bytecode VM.** saQut kendi IR'sine derler ve bir yorumlayıcı döngüyle
çalıştırır.
-**Makine-kodu JIT kapsam dışıdır** (terminoloji düzeltmesi: "JIT" demeyi
bırak). Öncelikler **determinizm + incelenebilirlik** (toolbox), ham hız değil.
- **Bellek kolaydır:** host (C++) heap'i; özel runtime allocator yok (v0).
- **C'ye transpile, geçerli bir İKİNCİ backend olarak ileride kalır** (frontend
backend-bağımsız, ADR-006).
- İleride makine kodu **gerçekten** istenirse: elle code generator yazmak yerine
**libgccjit / LLVM'e bağlan** (ADR-001'deki QBE/custom değerlendirmeleri o gün
için geçerli). Bu **çok uzak gelecektir.**
---
## ADR-016: FFI Seam — Host Fonksiyon Çağırma Deliği
### Bağlam
`print` bile bir "dış dünya" çağrısıdır; VM tek başına ekrana yazamaz, host'tan
bir fonksiyon çağırmalıdır. Bu ihtiyaç ya **kaza eseri** tek bir özel-durum
olarak gömülür, ya da **kasıtlı bir mekanizma** olarak tasarlanır.
### Karar
**IR/runtime tasarımına bilinçli bir FFI seam konur:** "host fonksiyonu çağır"
için tek, genel bir IR mekanizması (örn. `callhost <id>, args...`).
- `print` bu seam'in **ilk müşterisidir**, özel-durum değil.
- İleride tüm "batteries" (bkz. ADR-017) bu sınır üzerinden gelir: sıkıştırma/
kripto C kütüphaneleri buraya bağlanır.
- **Neden şimdi:** seam'i sonradan eklemek IR ve VM'i baştan değiştirmeyi
gerektirir; deliği bir kez doğru açmak ucuzdur. Mekanizmayı **şimdi** doğru
tasarla, içini sonra doldur.
---
## ADR-017: Batteries / Stdlib — Sınır Problemi (Ertelendi)
### Bağlam
Gerçek bir genel sürüm pil ile gelmeli (sıralama, sıkıştırma, kripto,
JSON/XML/HTML, ileride runtime/donanım, ses/görüntü/video). JSON/string
ergonomisi olmayan bir dil benimsenmez — bu doğru. Ama korku: pilleri çekirdeğe
gömmek **monolit** yaratır.
### Karar
**Pil = sınır (boundary) problemi, "zlib'i yeniden yaz" problemi değil.**
- Çekirdek: **küçük bir gerçek builtin kümesi** (`print`, temel zorunlular) +
**gerisi kütüphane/FFI.**
- **JSON/XML/HTML ayrıştırıcıları saQut'ta yazılabilir** (string + struct +
fonksiyon + kontrol akışı yeter) — ilk "gerçek program" demoları.
- **Sıkıştırma/kripto:** denenmiş C kütüphanelerine **FFI** ile bağlan. **Kripto
asla elle yazılmaz.**
- **Bugüne tek yansıması:** FFI seam'i (ADR-016) bırak. Gerisi **v0 kapsamı
dışıdır.** Sınır bir kez çizilir, piller üstünde sonsuza dek birikir.
---
## ADR-018: `interface` Ertelemesi (Reddedilmedi)
### Bağlam
Kullanıcı struct'ın yanında `interface` de istedi (crypto, compression, custom
data types, JSON, string için). `interface` alınmalı mı, ne zaman?
### Karar
**Şimdi `struct`, `interface` ise ertelenir (reddedilmez).**
**Neden ertelendi:** `interface`, `struct`'tan kategorik olarak ağırdır.
- Struct yalnızca alan yerleşimidir (field layout).
- Interface "bu metotları sağlayan herhangi bir tip" demektir → çağrı yerinde
somut tip **bilinmez****dinamik dispatch** → vtable / fat pointer (içsel
pointer, izinli) → ve bir interface değeri **herhangi bir tipi tutabilir**, bu
da **kaçma/yaşam-süresi (escape/lifetime) problemini yeniden açar** (ADR-019).
- Go bunu fat pointer + GC ile çözer; saQut **GC istemiyor.**
Kullanıcının saydığı her şey (crypto, compression, custom data, JSON, string)
**yalnızca struct + fonksiyonla** yapılabilir (C bunu kanıtlar). Dolayısıyla:
şimdi struct'ı al, interface'i ertele.
**Metot-çağrı şekeri** (`list.push(5)` → `push(list, 5)`) ileride **parser
seviyesinde, sıfır semantik maliyetli** bir desugaring olarak eklenebilir —
şimdi değil.
---
## ADR-019: Frontend ↔ Runtime Sorumluluk Ayrımı
### Bağlam
"Hangi CPU çekirdeği, hangi cihaz, ne zaman tetiklenir, hangi çıktı formatı"
gibi sorular nereye ait? Frontend'e mi, runtime'a mı?
### Karar
**Net ayrım, frontend'i runtime kaygılarıyla yükleme:**
- **Frontend:** **yapı ve anlam** — tip, scope, dataflow. (Bu yol haritasının
konusu.)
- **Runtime/backend:** çekirdek/cihaz/tetikleme/çıktı formatı.
**Neden:** bu ayrım, kullanıcının önem verdiği **modülerliği korur**; frontend
backend-bağımsız kalır (ADR-006), böylece IR+VM ve ileride C-transpile aynı
frontend'den beslenir. Ayrıca **value-semantics + no-escape** kararının (kaçan
referans/closure yok) bağımlılığı buradadır: bu sayede scope-tabanlı bellek
çalışır (ADR-014). Closure veya interface değerleri eklemek bu ayrımı ve bellek
modelini birlikte zorlar — ikisi de bu yüzden ertelendi.
---
## ADR-020: Değer vs Referans Semantiği — Bileşik Tipler Runtime'da Referanstır
### Bağlam
ADR-014/018/019 boyunca bellek modeli tek bir **taşıyıcı varsayıma** dayanıyordu:
> "kullanıcı pointer'ı yok + kaçan referans yok → array/struct scope'tan kaçamaz
> → scope-tabanlı bellek çalışır, **GC gerekmez**."
Bu varsayım, `interface`'in (ADR-018) ve closure'ın ertelenmesinin de ikinci
gerekçesiydi. Tasarım oturumunda **bilinçli olarak değiştirildi.** "Pointer yok"
ilkesinin gerçekte ne demek olduğu netleşti:
> **"Pointer/referans yok" = kullanıcıya `&`/`*` *sözdizimi* verilmez.**
> Bu bir *value-semantics* iddiası değildi; amacı sözdizimsel pointer kontrolünü
> kullanıcıdan almaktı. Derleyici ve runtime, bileşik değerleri her aşamada
> **referansla** taşır — aksi halde her atama/çağrı/dönüşte derin kopya yaşanır
> ve bağlı yapılar (node) imkânsızlaşırdı.
### Karar
**İki katmanlı semantik (JavaScript / Java / C# nesne modeli):**
| Kategori | Tipler | Atama / parametre semantiği |
|---|---|---|
| **Primitive** | `int`, `float`, `bool`, (`char` vb.) | **Saf değer** — kopyalanır |
| **Bileşik (referans)** | `struct`, `array`, `string`\*, (ileride `class`, `function`) | **Referans** — paylaşılır |
- `a=0; b=a; b=5``a` hâlâ `0` (primitive kopya). Fonksiyon parametresinde de aynı.
- `func(arr)` → array'in **kendisi** geçer; `func` içinde değişen çağıranı **etkiler**.
- `func(arr[0])` → eleman primitive → **kopya**; çağıranı **etkilemez**.
- \* `string`'in primitive-gibi mi (immutable değer) yoksa referans mı sayılacağı
ayrı bir alt-karar; #40 ile beraber netleşecek.
**`class` ve `function` tipleri sözdizimsel olarak rezerve** — şu an semantik
yok, backend'i ilgilendirmez; ileride referans tip olarak gelecekler. Lexer/parser
keyword'leri tanıyıp "henüz desteklenmiyor" diyebilir. (ADR-014'teki "class yok
sayılır" maddesi bu yönde yumuşatıldı: yok sayılmaz, rezerve edilir.)
### Bilinçli geri açtığımız problem: kaçma / yaşam-süresi
Bu karar, ADR-014'ün "scope-tabanlı bellek GEREKÇELİ / GC gerekmez" sonucunu
**iptal eder.** Referansla:
1. **Aliasing gerçek.** `b = a; b.x = 5``a.x` de değişir. "Takma ad yok, akıl
yürütmesi kolay" sadeliği takas edildi. **Determinizm korunur** — tek
iş-parçacığı, deterministik kayıt-tekrar / time-travel debug hâlâ doğal; takas
edilen yalnızca aliasing-özgürlüğüdür.
2. **Bileşikler scope'tan kaçar.** Bir node `return` edilebilir veya başka bir
struct'ın alanında saklanabilir → "scope çıkışında free" **artık yanlış.**
Sahiplik scope'a değil **erişilebilirliğe** bağlı.
3. **Döngüsel yapılar artık meşru ve istenen.** `struct Node { Node next; }`
ADR-011/014'te `E010` ile yasaktı (by-value → sonsuz boyut). Referansla alan
pointer-boyutlu → **sonlu** → bağlı liste / ağaç / graf **yazılabilir.** Bunlar
dilin hedef kullanımının kalbi: XML node'ları, JSON kalıpları, class'sız ORM.
**Sonuç:** `E010` revize edilmeli — referansla tutulan struct alanı için döngü
artık hata değildir.
### Bunun açtığı zorunlu problem (ayrı issue)
Döngüsel referans → naif **referans sayımı (`shared_ptr`) sızdırır.** Bu artık
"olabilir" değil, dilin **hedeflediği** yapıların (graf/döngü) doğrudan sonucu.
Bir geri-kazanım stratejisi (izleyici GC / döngü toplayıcı) **kesinlikle**
gerekecek. Bu güçlü mimari borç **#56**'da izlenir ve `karar-gerekli`. v1 motoru
`shared_ptr` ile başlayıp döngüyü **bilinçli ve belgeleyerek** sızdırabilir, ama
ürünleşmeden önce çözülmek zorundadır.
### İptal/revize edilen önceki kararlar
- **ADR-014** — "scope-tabanlı bellek GEREKÇELİ / GC gerekmez" sonucu **iptal.**
Bellek artık scope'a değil erişilebilirliğe bağlı; geri-kazanım stratejisi #56.
- **ADR-018 / ADR-019** — `interface` / closure'ı ertelemenin "kaçma problemini
yeniden açar" gerekçesi **artık geçersiz** (problem zaten açık). Bu ikisini
*daha kolay* alınabilir kılar — ama hâlâ kapsam dışı, sadece engeli değişti.
- **ADR-011** — `E010` döngüsel struct kuralı revize edilecek (yukarı bkz.).
---
## ADR-021: Null Güvenliği — `Type?` Nullable + Akış-Duyarlı Null Analizi
### Bağlam
ADR-020 ile bileşik tipler referans oldu. Referans, "gösterecek bir şey yok"
durumunu (bağlı listenin sonu, başlatılmamış alan) **zorunlu** kılar. "null her
yerde" (Java/C#/JS) milyar dolarlık hatadır: null-deref çalışma zamanında patlar.
saQut'un kimliği "kafes — derleyici/VM seni korur" → bunu derleme zamanında
yakalamak istiyoruz.
### Karar
**Kotlin/Swift modeli: varsayılan null-OLAMAZ, nullable açıkça `?` ile.**
- `Node a` → asla null olamaz; başlatılması zorunlu.
- `Node? a` → null olabilir; başlatılmazsa değeri **`null`**.
- `null` literali yalnızca `T?` tipine atanabilir; `Node a = null`**derleme hatası**.
- `T?` üstünde doğrudan alan/eleman erişimi (`a.next`) → **derleme hatası**
(önce null-kontrolü şart).
### Atama/operand kuralı — `T <: T?` (tek yönlü), katı
**Alt-tip:** `T <: T?`. Yani:
- `int? a = 5;` → ✓ (int → int?, **genişletme serbest**).
- `int a = bir_int?;` → ✗ (int? → int, **daraltma yasak**).
**Katı operand kuralı:** non-null bir bağlamda — atamanın sol tarafı, **her
operatör operandı**, non-null bekleyen bir argüman — değer **statik olarak non-null**
olmalı. `int a = b + c + d`'de `b/c/d`'den **biri bile** nullable ise → **derleme
hatası.** Sembol tablosu/akış görünümü seviyesinde: `notnull = notnull + notnull + …`.
Sezgi yok, deterministik. (`notnull + notnull` → `notnull`.)
### Akış-duyarlı null analizi (flow-sensitive narrowing)
`T?` bir değişken **program noktasına göre** "kesin non-null" kanıtlandıysa
daraltılır:
```c
Node? a = ...;
// a.next; // E0xx: a null olabilir
if (a != null) {
a.next; // OK — bu dalda a, Node'a daraltıldı
}
// a.next; // yine hata (daldan çıkıldı)
if (a == null) return; // guard / erken çıkış
a.next; // OK — buradan sonrası kesin non-null
```
`if` bu sistemin **bel kemiğidir** — sadece büyük/küçük/eşitlik değil, nullable
aklamanın da aracı. **İki form da desteklenir:**
1. **Nested (blok-kapsamlı):** `if (a != null) { /* a: T burada */ }`. Ayrıca
`if/else`'in zıt dalı, `while (a != null) { … }`.
2. **Sıralı (guard / erken-çıkış):** `if (a == null) return; /* a: T bundan sonra */`.
Dal kesin çıkıyorsa (`return`/`throw`/`break`/`continue`) negasyonu **ardışık**
koda taşınır.
**Mekanik:** CFG üzerinde ileri-yönlü dataflow; her nullable değişken için kafes
`{MaybeNull, NonNull}`. Koşullarda daraltma (`!= null`, `== null` guard, `&&`
kısa-devre sağ tarafı); kesin-çıkış dalları negasyonu ardına taşır; birleşme (join)
muhafazakâr (bir daldan MaybeNull gelirse MaybeNull); atama RHS'e göre sıfırlar.
> **Karmaşıklaştırma sınırı:** narrowing yalnızca **doğrudan test edilen değişken**
> için tanınır (`x == null`/`x != null`). **Alias takibi YOK** (`y = x; if (y != null)`
> → x daralmaz) ve keyfi teorem-ispatı yok. Bu, derleyiciyi basit tutarken yaygın
> durumların hepsini kapsar → developer **uzun/karmaşık kod yazmak zorunda kalmaz.**
> **Runtime maliyeti SIFIR** — tamamen derleme-zamanı analizi; üretilen kodda
> fazladan kontrol yok.
### Kaçış kapısı YOK — `!` ve `??` YASAK
Null **yalnızca görünür kontrol akışıyla** (yukarıdaki `if` narrowing) aklanır.
Gizli runtime null-aklama operatörleri **yasaktır:**
-**`x!`** (non-null iddiası) — "compiler'a güvenme, runtime'da kontrol et"
= statik garantiyi delen gizli backdoor. *(ADR-021'in ilk taslağındaki `a!`
KALDIRILDI.)*
-**`x ?? default`** (elvis), **`x?.field`** (güvenli çağrı) — null durumunu
sessizce gizleyen şeker.
> Ayrım: `as int`'in başarısızlıkta fırlatması yasak **değil** — o bir null-backdoor
> değil, kendiliğinden başarısız olabilen bir *dönüşüm* (ADR-026).
### Frontend her şeyi kesin çözer (backend-bağımsızlık)
Nullability **tamamen frontend'de** çözülür; tüm null-güvenlik hataları IR'den
**önce** verilir. Backend'ler (IR+VM, ileride C-transpile) null-güvenliği **yeniden
analiz etmez** — garantiyi hazır devralır (ADR-006/019). Bu sayede: well-typed saf
saQut kodu **statik null-güvenlidir** → non-null referans deref'i runtime null-kontrolü
**gerektirmez** (perf + sadelik). Runtime null-deref hatası (ADR-025) bu yüzden
geriye esas olarak **FFI sınırı** (host non-null sözünü çiğnerse) ve savunma amaçlı
backstop olarak kalır — saf saQut kodu bunu üretmez.
### Mimari yeri
Bu, saQut'un ilk gerçek **akış-duyarlı** analizidir. **Yapısal kontrol akışı**
üstünde (AST + structured CFG) yapılabilir; tam SSA gerektirmez → **#2 (CFG/SSA
gerekli mi?)** için somut veri: şimdilik yapısal akış analizi yeter. **#20**
(akıllı diagnostic) bu analizden beslenir ("burada null olabilir, çünkü …").
---
## ADR-022: Bellek Geri-Kazanımı — Basit Deterministik Mark-Sweep + GC-Hazır Nesne Modeli
### Bağlam
ADR-020 referans semantiği → döngüsel yapılar (#56). Kısıtlar: GC **basit ve
deterministik** olmalı, "karmaşık ve rastgele" istenmiyor. Ayrıca bu, **geç
değiştirilmesi en pahalı** karardır (nesne modeline işler) → topuğa sıkmamak
kritik.
### Önce yanlış-eşleştirmeyi temizle
**`null`/`?` GC'yi zorlaştırmaz.** Nullable tamamen derleme-zamanı/tip meselesidir;
runtime'da null referans sadece "boş işaretçi" → GC için *daha kolay* (izlenecek
nesne yok). null ile GC **dik (orthogonal)**; aralarında gerilim yoktur.
### Seçenekler ve neden mark-sweep
| Strateji | Döngü | Basitlik | Topuğa-sıkma riski |
|---|---|---|---|
| Refcount (`shared_ptr` her yerde) | ❌ sızdırır | başta basit | **Yüksek** — node dilinde döngü kaçınılmaz; üstüne döngü toplayıcı = CPython karmaşıklığı (tam "karmaşık/rastgele") |
| **Mark-sweep, taşımasız, stop-the-world** | ✅ | **en basit *doğru* GC** | **Düşük** — gelişmiş GC'lerin tabanı; üstüne eklenir, yeniden yazılmaz |
| Generational / incremental / compacting | ✅ | karmaşık (write barrier, remembered set) | pause'lar belirsizleşir = istenmeyen "rastgele" |
**Karar: taşımasız (non-moving), stop-the-world, basit mark-sweep.**
- Döngüleri **bedavaya** toplar (izleme döngü umursamaz) → #56'yı gerçekten çözer.
- **Deterministik:** GC belirli safepoint'lerde çalışır (ör. her N tahsiste) →
kayıt-tekrar / time-travel bit-aynı kalır ("cage" korunur). "Rastgele" değil.
- Taşımasız → işaretçi düzeltme / barrier yok → VM'in geri kalanı GC'ye katılmak
zorunda değil. *Crafting Interpreters*'ın `clox`'u tam bunu yapar (~birkaç yüz satır).
### Topuğa-sıkmama kuralı — nesne modelini ŞİMDİ GC-hazır kur
Asıl risk GC'yi *yazmak* değil, nesne modelini sonradan ona uyduramamaktır. O
yüzden **bugünden** (toplama yokken bile):
1. Her heap nesnesine küçük **header**: tip tag + mark biti + tüm-nesneler listesi için `next`.
2. VM **kök (root) sayımı** yapabilsin: operand stack, frame local'leri, global'ler.
3. Bir nesne **içerdiği referansları** sayabilsin: referans-tipli struct alanları,
referans-tipli array elemanları.
Bu üçü hazırsa "mark-sweep'i aç" **lokal bir ekleme** olur, nesne-modeli yeniden
yazımı değil.
### Aşamalandırma (#56'nın yönü)
- **v1 (şimdi):** GC-header'lı tahsis + intrusive tüm-nesneler listesi + kök sayımı.
**Toplama yok** (program sonunda hepsini bırak / arena). Fibonacci/test ölçeğinde
sorunsuz; kısa programlar sızıntıdan etkilenmez.
- **v2 (#56 ciddileşince):** aynı header+kök+çocuk-sayımı üstünde mark-sweep'i aç.
Model yeniden yazılmaz.
- **`shared_ptr`'dan kaçın:** v1'de bile her referansa refcount gömmek, sonra
mark-sweep için **sökmek** ayrı bir topuğa-sıkmadır. Baştan GC-header modeli kur,
sadece henüz toplama.
### Performans notu — asıl "katil" nerede?
- **Nullability / null:** runtime maliyeti **sıfır** — katil değil.
- **Referans modeli:** her bileşik heap'te + işaretçi dolaylılığı → düzenli ama
yönetilebilir maliyet; ileride **escape analizi** ile kaçmayan nesneleri stack'e
alıp *semantiği bozmadan* hızlandırılır (opt-in, sonra).
- **Tek yüksek-değişim-maliyetli karar = GC.** Onu da (a) basit mark-sweep seçip
(b) modeli baştan GC-hazır kurarak de-risk ettik. **Kaçınılacak gerçek katil:
refcount'u kalıcı model yapmak.**
---
## ADR-023: Eşitlik Semantiği — Referanslarda Kimlik Eşitliği (`==`)
### Bağlam
ADR-020 ile bileşik tipler referans. `==` / `!=` referans tipler için ne yapsın?
Yapısal (derin) eşitlik sezgisel ama üç sorunu var: (1) büyük yapıda **derin
gezinme maliyeti**, (2) yeni açtığımız **döngüsel grafta sonsuz döngü** riski
(ziyaret-takibi şart), (3) seçtiğimiz referans modeliyle **tutarsız**.
### Karar
**Kimlik eşitliği (A):**
| Kategori | `==` davranışı |
|---|---|
| Primitive (`int`/`float`/`bool`) | **değer** karşılaştırması (`3 == 3`) |
| Referans (`struct`, `array`) | **kimlik** — aynı nesne mi? (işaretçi aynılığı) |
| `string` | ⏸️ **#40'a bağlı** — aşağıdaki nota bak |
| `null` | `null == null` → true; `null == nesne` → false; `a == null` null-daraltma deyimi (ADR-021) |
İçerik karşılaştırması istenirse **ayrı, niyeti görünür** bir mekanizmayla gelir
(ileride builtin `deepEquals()` / PHP'nin `==` vs `===` vs `clone` ailesi gibi) —
asla sessizce `==`'e bağlanmaz. Gerekçe: deepEqual'ı `==`'e bağlamak büyük/döngüsel
yapılarda performans ve sonsuz-döngü tuzağıdır; "cam kutu, sürpriz yok" kimliğiyle
de çelişir.
### ⚠️ String istisnası (Java gotcha'sı)
Saf kimlik eşitliğini string'e de uygularsak `"abc" == "abc"`**false** olur —
Java'nın en çok sövülen hatası. Çoğu dil string'i istisna yapar (JS'te string
primitive → içerik; C# overload; Python intern). Bu yüzden **string'in `==`'i
içerik eşitliği olmalı**, ki bu string'i **immutable değer-tipi** olarak modellemeyi
güçlü biçimde öneriyor (bkz. #40). ADR-023 struct/array'i kilitler; string'in `==`'i
#40'ta netleşir ama **varsayılan yön: içerik eşitliği.**
### Açık (ileride, çok uzak — şimdi karar değil)
- **`obj == obj`'i hata/uyarı yapmak:** kullanıcıyı niyetini açık yazmaya zorlamak
(kimlik mi içerik mi). Daha katı bir duruş; v0'da `==` = kimlik serbest.
- **Kullanıcı-tanımlı eşitlik (OOP'siz):** ileride bir tip için `equals(T,T)->bool`
konvansiyonu veya benzeri ile `==`'i kullanıcının tanımlamasına izin vermek —
operator-overload'un OOP'siz karşılığı. Çok uzak.
---
## ADR-024: String — Immutable Değer-Tipi, İç Temsil UTF-8
### Bağlam
ADR-020 string'i "bileşik (referans)" listesine `?` ile koymuştu; ADR-023 string
`==`'inin **içerik** olmasını istedi (Java gotcha'sından kaçınmak için). İkisi de
string'i değişmez-değer modeline itti.
### Karar
**String = immutable (değişmez) değer-tipi; iç temsil UTF-8 bayt.**
- **Immutable:** oluşturulduktan sonra içeriği değişmez; `s = s + "x"` **yeni**
string üretir, eskisini değiştirmez.
- **`==` içerik eşitliği** (ADR-023 istisnası). Paylaşılınca değişmediği için
içerik-eşitliği güvenlidir; aliasing sürprizi yok (JS'in string'i primitive gibi
davranmasının sebebi budur).
- **GC dostu:** serbestçe paylaşılır / intern edilebilir.
- **İç temsil UTF-8** (Rust/Go/Swift hattı): kompakt, web-doğal, ASCII'de ucuz.
`s[i]` **karakter** indeksi O(1) **değildir** → bayt / scalar / grapheme erişimi
**açıkça** ayrılır; sahte O(1) vaat edilmez (Java/JS'in "uzunluk emoji'de yalan
söylüyor" sürprizinden kaçın). Host tarafında `std::string` ham bayt olarak oturur.
- **Verimli birleştirme** için ileride ayrı **builder** tipi (StringBuilder / `join`)
— çekirdeği kirletmeden, döngüde O(n²)'den kaçınmak için.
### Etkilenen
- **#40** (string işlem yüzeyi) bu kararla netleşti; **#9** (iç temsil) = UTF-8.
- ADR-020'deki string `?` işareti → "değer-tipi" olarak çözüldü.
---
## ADR-025: Hata Yönetim Modeli — Struct-Tabanlı Yakalanabilir Hatalar (Swift-tarzı)
### Bağlam
ADR-020 (struct = referans) → null bir struct alanına erişim/yazma ihtimali doğdu:
klasik NullPointerException. ADR-021 statik analizi *kanıtlayabildiğini* derleme
zamanında yakalar, ama `!` iddiası ve kanıtlanamayan durumlar (struct alanı,
cross-fonksiyon) için bir **runtime backstop** gerekir. Ayrıca array OOB, /0 gibi
faults. Java/C#/JS bunları **yakalanabilir** hata yapar — ama OOP exception
hiyerarşisi (`extends Exception`) bizde yok.
### Karar
**Yakalanabilir, struct-tabanlı hata modeli — OOP'siz.**
Hata *değeri* Swift gibi (düz struct, hiyerarşi/extend yok); *görünürlük* Java/C#/JS
gibi (**unchecked** — fonksiyon işaretlenmez, klasik `try{}catch{}`). "Exception'ın
tanıdık catch-and-jump ergonomisi + OOP'suz değer."
1. **Hata değeri = standart built-in struct** — extend yok, OOP yok, deterministik:
```
struct Error {
int line; // hata satırı
int col; // sütun ("char" tip adıyla çakışmaması için col)
string message; // insan-okunur (derleyicinin W/E kataloğundan)
string trace; // stacktrace, en içten dışa
string code; // makine-okunur W/E kodu (E010 vb.) — JSON/toolbox filtresi
}
```
2. **try/catch (unwind + jump):** hata oluşunca en yakın çevreleyen `catch`'e
zıplanır; `catch (e)``e : Error`.
3. **Runtime null-deref = yakalanabilir hata** (NPE analoğu). ADR-021 statik
analizinin **backstop'u**: `a!` patlayınca + analizin kanıtlayamadığı durumlar.
Array OOB ve /0 da aynı kapıdan.
4. **`throw`** ile kullanıcı da hata kaldırabilir (`Error` doldurup).
5. **Determinizm:** unwind deterministik; stacktrace frame'lerden üretilir;
time-travel/replay handle eklenebilir.
### Görünürlük — KARAR: (ii) görünmez / unchecked (Java/C#/JS usulü)
**Fonksiyonlar işaretlenmez.** "Bu hata yapabilir / yapamaz" anotasyonu **YOK**
(C++'ın `noexcept`/`constexpr` benzeri kirlilik istenmiyor). Çağrıda `try f()`
işareti de yok. **Klasik `try { ... } catch (e) { ... }` bloğu** — "anam babam usulü".
**Gerekçe:** developer'a **güven** + insanların derin try-catch alışkanlığını bozmamak
(sözdizimini tanıdık tut, içgüdüye dokunma). Hatalar zaten çoğunlukla FFI, bellek
dolması ve derleyici-içi durumlardan doğar; her çağrıyı işaretlemenin bedeli faydadan
büyük.
> Not: Bu, `Type?` (explicit nullable) ile **bilinçli** felsefi ayrışmadır — null
> *tipte* görünür, ama hata akışı *blok* düzeyinde tanıdık tutulur. Reddedilen (i):
> Swift/Zig'in imza-işaretli + çağrıda `try f()` modeli.
### Stacktrace mekaniği (modelden bağımsız önkoşul)
- Her `CallFrame``IRFunction` + komut işaretçisi; **IR'a satır tablosu**
(komut index → kaynak konum) eklenir (önce taşıyıp taşımadığı doğrulanmalı).
- panic/throw'da frame stack gezilir → `fonksiyon + konum`, en içten dışa → `trace`.
- Sunum: derleme-zamanı diagnostic ile **aynı kabuk** (kod + mesaj + konum +
"nasıl düzelt" #20), hem insan hem **JSON** (toolbox: her hata yapılandırılmış nesne).
- Farklılaştırıcı: deterministik → trace'e adım indeksi → hataya **geri sar**.
### İlişkili güncellemeler
- **ADR-014 "tuple yok" → "tuple ERTELENDİ"** (reddedilmedi; çoklu-dönüş kodu
spagettileştirir, şimdilik uzak ama masada — `interface` gibi).
- **`finally` yerine ileride `defer`** (GC'li, RAII'siz dilde daha temiz). Ayrı küçük karar.
- ADR-021 ile uyum: statik analiz provable null'ı yakalar; bu hata onun backstop'u + `!`.
---
## ADR-026: Tip Dönüşümü — `as` (Skaler/String), Başarısızlık Hedef Tipinin Nullable'lığıyla
### Bağlam
ADR-010 "gizli int↔float yok" → değişken-değişken dönüşüm **açık** olmalı. float
runtime'ı var ama cast sözdizimi yoktu → int↔float dönüşümü imkânsızdı. Ayrıca
elimizde null (ADR-021) + hata (ADR-025) modelleri var; cast bunlarla örtüşmeli.
### Karar
**Sözdizimi: `as` (infix, sola-bağlı).** `deger as int`.
- Sola-bağlı olduğu için zincir **lineer** okunur: `a as int as string` =
`((a as int) as string)`, parantez gerekmez (fonksiyon-stili `int(float(a))`'nın
iç içe çirkinliği yok).
- `int(x)` fonksiyon-stili **reddedildi** ("int adlı fonksiyon mu, cast mı"
belirsizliği); C-tarzı `(int)x` ve `static_cast<>` reddedildi.
**Kapsam: yalnızca skaler + string** (`int`/`float`/`bool`/`string` arası).
- **Struct/array cast'e GİRMEZ.** Farklı struct'lar ayrı tiplerdir; "dönüşümleri"
geliştiricinin yazdığı **açık yapıcı fonksiyonlarla** olur (`Employee yap(Person p)`).
Gerekçe: yapısal/duck eşleme veya reinterpret = derleyiciyi karmaşıklaştırır +
sessiz alan kaybı = hataya açık. OOP'siz "cage" kimliğiyle uyumsuz.
**Başarısızlık davranışı = HEDEF TİPİN nullable'lığı** (ayrı `as?` operatörü YOK):
- `x as int` → hedef non-null → başarısızsa **`Error` fırlatır** (ADR-025), sonuç `int`.
- `x as int?` → hedef nullable → başarısızsa **`null` döner**, sonuç `int?`.
Nullable her zaman **tipte** (`?`) yaşar, ayrı operatör icat edilmez. Sonra `int?`'i
`int`'e çevirmek için **narrowing** (`if`) gerekir — `!`/`??` yasak (ADR-021).
### Dönüşüm matrisi
| Dönüşüm | Hatasız mı? | Not |
|---|---|---|
| `int → float` | ✅ hatasız | büyük int'te kesinlik kaybı olabilir, patlamaz |
| `int → string`, `float → string` | ✅ hatasız | biçimlendirme |
| `string → int`/`float` | ⚠️ fallible | parse; `"abc"``as int` fırlatır / `as int?` null |
| `float → int` | ⚠️ fallible | sonlu & aralık-içi: **sıfıra doğru kırpılır** (`1.71→1`, `-1.71→-1`); NaN/Inf/taşma → fırlatır / null |
| `bool ↔ int` | (karar) | başta yasak tutmak en güvenlisi; gerekirse açılır |
### Örnek (ADR-021 ile birlikte)
```
int a = 1.71 as int?; // ✗ DERLEME HATASI: int? → int (daraltma); cast başarılı olsa bile statik tip int?
int a = 1.71 as int; // ✓ a = 1 (kırpma); başarısızsa Error
int? a = 1.71 as int?; // ✓ tipler eşit
```
---
## Kararların Özet Tablosu
| ADR | Konu | Karar |
|---|---|---|
| 006 | Frontend mimarisi | Çok-aşamalı; frontend/middle-end/backend katmanları |
| 007 | Analiz vs optimizasyon | Analiz yerinde; `ast` komutu klon üstünde dönüştürür (öncesi/sonrası karşılaştırması); `run`/`ir` yerinde optimize eder (klon yok); sembol bağları salt-okunur paylaşım (remap gerekmez) |
| 008 | Optimizasyon konumu | Basitler AST'de, dataflow gerektirenler IR'de |
| 009 | Pass yönetimi | Fixpoint döngüsü, toggle'lı; monotonluk/iterasyon-tavanı değişmezi; akışa-bağlı analiz tur başına tazelenir |
| 010 | Tip sistemi | Minimal+genişletilebilir Type; gizli dönüşüm yok; Error tipi; tamsayı literali bağlama-göre tiplenir |
| 011 | Scope/forward ref | Global'de forward ref (fonksiyon/struct), ama global başlatıcı declare-before-use; lokal declare-before-use; döngüsel struct → `E010` |
| 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/closure yok; struct+array+tipli fonksiyon var; scope-tabanlı bellek gerekçeli |
| 015 | Çalıştırma modeli | IR + bytecode VM; makine-kodu JIT kapsam dışı |
| 016 | FFI seam | Kasıtlı "host fonksiyonu çağır" mekanizması; `print` ilk müşteri |
| 017 | Batteries/stdlib | Sınır problemi; küçük builtin + FFI/kütüphane; ertelendi |
| 018 | `interface` | Ertelendi (reddedilmedi); struct+fonksiyon yeter |
| 019 | Frontend↔runtime | Frontend yapı+anlam; çekirdek/cihaz/çıktı runtime'a ait |
| 020 | Değer/referans semantiği | Primitive=değer, bileşik (struct/array/string)=referans; "pointer yok"=`&`/`*` sözdizimi yok; kaçma/lifetime problemi bilinçli açıldı → GC borcu (#56); ADR-014'ün "GC gerekmez" sonucu iptal |
| 021 | Null güvenliği | `Type?` nullable, varsayılan non-null; akış-duyarlı null analizi (compile-time, runtime maliyeti sıfır); `!` runtime-kontrollü non-null iddiası |
| 022 | Bellek geri-kazanımı | Basit taşımasız stop-the-world mark-sweep (deterministik); nesne modeli baştan GC-hazır (header+root+child); v1 toplamasız, v2 mark-sweep; refcount kalıcı model DEĞİL; #56'nın yönü |
| 023 | Eşitlik semantiği | `==` = primitive değer / referans (struct,array) **kimlik**; deepEqual asla `==`'e bağlanmaz (ayrı `deepEquals()`); string `==` içerik (→ #40, Java gotcha'sından kaçın); `obj==obj` hata + kullanıcı-tanımlı eşitlik = uzak gelecek |
| 024 | String | Immutable değer-tipi, iç temsil **UTF-8**; `==` içerik; mutasyon yeni string üretir; bayt/scalar/grapheme açıkça ayrı; verimli birleştirme için ileride builder; #40/#9'u çözer |
| 025 | Hata yönetimi | Struct-tabanlı yakalanabilir hata (değer Swift gibi, OOP yok); standart `Error{line,col,message,trace,code}`; klasik `try{}catch{}` **unchecked** (fonksiyon işaretsiz, Java usulü); runtime null-deref/OOB yakalanabilir (esasen FFI backstop); deterministik stacktrace (IR satır tablosu); tuple→ertelendi; finally→`defer`; #57 |
| 026 | Tip dönüşümü | `as` (infix, sola-bağlı), yalnızca skaler+string; struct/array cast YOK (elle yapıcı fonksiyon); başarısızlık hedef nullable'lığıyla (`as int` fırlatır / `as int?` null); float→int kırpma; #42 |