OCA.DEV
arrow_back ALL PROJECTSTÜM PROJELER

payflow

RUNS IN YOUR BROWSER

A small programming language for subscription billing, with its own lexer, parser, type checker and interpreter — all hand-written, no parser generator, no dependencies. Money carries its currency in the type, so adding dollars to euros is a compile error rather than a support ticket. Abonelik faturalandırması için küçük bir programlama dili; kendi sözcük çözümleyicisi, ayrıştırıcısı, tip denetleyicisi ve yorumlayıcısıyla — hepsi elle yazıldı, ayrıştırıcı üreteci yok, bağımlılık yok. Para birimi tipin içinde taşındığı için dolara euro eklemek bir destek kaydı değil, derleme hatası.

4 pipeline stages, hand-written boru hattı aşaması, elle yazılmış
50 unit tests, all passing birim testi, hepsi geçiyor
0 third-party dependencies üçüncü parti bağımlılık
8 precedence levels in the grammar gramerde öncelik seviyesi
The split operator, and the two rounding rules it could have had

This figure is drawn by a script that first runs the program through the interpreter and refuses to render if the answer is no longer 8.91. A figure that cannot go stale is worth more than a screenshot. Bu şekil, önce programı yorumlayıcıdan geçiren ve cevap artık 8.91 değilse çizmeyi reddeden bir script tarafından üretiliyor. Bayatlayamayan bir şekil, ekran görüntüsünden daha değerlidir.

Why a language and not a library Neden kütüphane değil de dil

A library can offer a Money class and a rounding helper. What it cannot do is make the wrong program fail to compile. Three billing concepts are constructs of the language here, and each one moves a class of bug from run time to compile time. Bir kütüphane Money sınıfı ve yuvarlama yardımcısı sunabilir. Yapamadığı şey, yanlış programın derlenmesini engellemektir. Burada üç faturalandırma kavramı dilin yapı taşı ve her biri bir hata sınıfını çalışma zamanından derleme zamanına taşıyor.

01 // MONEY

Currency lives in the type Para birimi tipin içinde

9.99 USD + 2.50 EUR
Type error at line 2:
cannot + money in
different currencies
(USD vs EUR)

money is not a float with a label stapled on. Where the currencies are known statically the checker rejects the program, and there is no coercion between money and float in either direction. money, üzerine etiket iliştirilmiş bir float değil. Para birimleri statik olarak bilindiğinde denetleyici programı reddediyor ve money ile float arasında hiçbir yönde dönüşüm yok.

02 // PAYWALL

Exhaustiveness is grammar Bütünlük gramerin işi

paywall Onboarding {
  when region == "EU" { ... }
}  <- no default

Parse error at line 13:
every `paywall` must end
with a `default` clause

The default clause is written into the grammar rule itself, not checked afterwards. A paywall with an unhandled region does not parse, so it cannot reach a user with a blank screen. default bloğu sonradan denetlenmiyor, doğrudan gramer kuralının içine yazılmış. Karşılanmayan bölgesi olan bir paywall ayrıştırılamıyor, dolayısıyla kullanıcıya boş ekranla ulaşamıyor.

03 // SPLIT

Deductions chain left to right Kesintiler soldan sağa zincirlenir

price split storeFee split vat

// binds looser than + and -
total + bonus split fee
// == (total + bonus) split fee

The store takes its cut first, then VAT comes off the remainder. Placing split below addition means an auditor reading left to right gets the same answer the interpreter does. Önce mağaza payını alır, sonra kalandan KDV düşer. split'i toplamanın altına koymak, soldan sağa okuyan bir denetçinin yorumlayıcıyla aynı cevaba varması demek.

Run it yourself Kendin çalıştır

This is not a simulation of the language. The five Python modules from the repository are loaded into your browser and executed there, so the errors below are the ones the real type checker raises. The runtime is about 10 MB and downloads only when you press RUN. Bu, dilin bir taklidi değil. Repodaki beş Python modülü tarayıcına yüklenip orada çalıştırılıyor, yani aşağıdaki hatalar gerçek tip denetleyicisinin ürettiği hatalar. Çalışma zamanı yaklaşık 10 MB ve yalnızca ÇALIŞTIR'a bastığında iniyor.

—
program.pf
press RUN

The AST and TOKENS tabs show the same structures the CLI prints with --dump-ast. Ctrl+Enter runs without reaching for the button. AST ve TOKENS sekmeleri, komut satırının --dump-ast ile bastığı yapıların aynısını gösteriyor. Ctrl+Enter, butona uzanmadan çalıştırır.

The example that was proving nothing Hiçbir şey kanıtlamayan örnek

The specification says split rounds after every step, not once at the end. The repository shipped an example named after that rule, using a 30% platform cut and 18% VAT. Spesifikasyon, split'in her adımdan sonra yuvarladığını söylüyor; sonda bir kez değil. Repoda bu kuralın adını taşıyan bir örnek vardı: %30 platform payı ve %18 KDV ile.

Both rules return 5.73 USD on those numbers. The example demonstrated the rule it was named after only in the sense that it did not contradict it — its own comment admitted as much. I picked percentages where the rules disagree and added a second example, so the claim is now settled by running it rather than by trusting the comment. O sayılarda iki kural da 5.73 USD veriyor. Örnek, adını taşıdığı kuralı ancak onunla çelişmediği ölçüde gösteriyordu — kendi yorum satırı da bunu kabul ediyordu. Kuralların ayrıştığı yüzdeleri seçip ikinci bir örnek ekledim; artık iddia, yoruma güvenerek değil çalıştırılarak karara bağlanıyor.

This is the same failure as a test that passes for the wrong reason. The number was right; the example simply could not tell the two candidates apart. Bu, yanlış sebepten geçen bir testle aynı hata. Sayı doğruydu; örnek sadece iki adayı birbirinden ayırt edemiyordu.

input per-step at end verdict
9.99 split 30% split 18% 5.73 5.73 tells nothing
9.99 split 3% split 8% 8.91 8.92 decisive

Load "rounding divergence" in the playground above to reproduce the second row. İkinci satırı yeniden üretmek için yukarıdaki playground'da "rounding divergence" örneğini seç.

Four stages, four error messages Dört aşama, dört hata mesajı

Each stage is its own module with its own exception type, and the driver turns any failure into one line naming the phase and the line number. Nothing is caught in a place that cannot say what went wrong. Her aşama kendi istisna tipiyle ayrı bir modül; sürücü, herhangi bir başarısızlığı aşamayı ve satır numarasını söyleyen tek satıra çeviriyor. Hiçbir şey, neyin bozulduğunu söyleyemeyecek bir yerde yakalanmıyor.

01 // LEXER lexer.py

Source to tokens. Percent is one token; money and duration are assembled later, so the lexer never has to know the currency list. Kaynaktan token'lara. Yüzde tek token; money ve duration sonradan birleştiriliyor, böylece lexer'ın para birimi listesini bilmesi gerekmiyor.

Lex error at line 4:
unexpected character '@'
02 // PARSER parser.py

Recursive descent. Left-associativity comes from loops, not from recursing before the operator is consumed — the usual way a hand-written parser turns out right-associative by accident. Özyinelemeli iniş. Sol birleşimlilik döngülerden geliyor, operatör tüketilmeden özyinelemeden değil — elle yazılmış bir ayrıştırıcının kazara sağ birleşimli çıkmasının olağan yolu budur.

Parse error at line 3:
comparison operators do not chain
03 // TYPES type_checker.py

Two passes, so a paywall can reference a plan declared further down. Records use name equivalence: identical fields are still different types. İki geçiş, böylece bir paywall daha aşağıda tanımlanmış bir plana atıf yapabiliyor. Record'lar isim denkliği kullanıyor: aynı alanlar hâlâ farklı tipler.

Type error at line 26:
expects Plan, got RefundRule
04 // INTERPRETER interpreter.py

Tree-walking, with money held as Decimal in the currency's minor unit. No float ever touches a price, so no drift accumulates across a chain. Ağaç yürüyen; para, para biriminin alt biriminde Decimal olarak tutuluyor. Hiçbir float fiyata dokunmuyor, dolayısıyla zincir boyunca kayma birikmiyor.

Runtime error at line 7:
division by zero

What it cannot do Yapamadıkları

SCOPED OUT

Records can be passed, not built Record'lar geçirilebiliyor, inşa edilemiyor

A record type can be declared, named in a parameter and read with field access, but there is no expression that constructs one. The type checker is complete on records; the language around them is not. Bir record tipi tanımlanabiliyor, parametrede adlandırılabiliyor ve alan erişimiyle okunabiliyor; ama onu inşa eden bir ifade yok. Tip denetleyicisi record'lar için tam; etrafındaki dil değil.

BY DESIGN

No assignment, so no loops Atama yok, dolayısıyla döngü de yok

Removing assignment makes every function pure and auditable in isolation, which is what a billing rule should be. The cost is real: there is no accumulator, so anything iterative has to be expressed by composition. Atamayı kaldırmak her fonksiyonu saf ve tek başına denetlenebilir yapıyor; bir faturalandırma kuralının olması gereken de bu. Bedeli gerçek: biriktirici yok, dolayısıyla yinelemeli her şey bileşimle ifade edilmek zorunda.

CAUGHT LATE

The spec used syntax the parser lacked Spesifikasyon, ayrıştırıcıda olmayan sözdizimini kullandı

The design document used p.price as a worked example before field access existed in the grammar. Writing the type-checker tests surfaced it, because they were the first thing to approach the spec from a new angle. Tasarım dokümanı, gramerde alan erişimi yokken p.price'ı çözümlü örnek olarak kullanıyordu. Bunu tip denetleyicisi testlerini yazmak ortaya çıkardı, çünkü spesifikasyona yeni bir açıdan yaklaşan ilk şey onlardı.

LEAKS TO RUN TIME

The money annotation has no currency money anotasyonu para birimi taşımıyor

A parameter is declared money, not money(USD), so the checker only rejects mixed currency where both sides are statically known. Pass a USD and a EUR into the same function and the failure arrives at run time instead — and its message starts with "internal:", because the interpreter considers this a case the checker was supposed to catch. Currency-parameterised types would close the gap; the playground's last two examples show both sides of it. Bir parametre money(USD) değil money olarak tanımlanıyor, dolayısıyla denetleyici karışık para birimini yalnızca iki taraf da statik olarak biliniyorken reddediyor. Aynı fonksiyona bir USD ve bir EUR geçir, hata bu kez çalışma zamanında gelir — ve mesajı "internal:" ile başlar, çünkü yorumlayıcı bunu denetleyicinin yakalaması gereken bir durum sayıyor. Para birimiyle parametrelenmiş tipler bu boşluğu kapatırdı; playground'daki son iki örnek meselenin iki yüzünü de gösteriyor.

NOT MEASURED

No performance claims Performans iddiası yok

Nothing here is benchmarked. A tree-walking interpreter on arbitrary-precision decimals is slow by construction, and the design accepts that openly — paywalls are evaluated about once per session. I have not measured it, so I do not quote a number. Burada hiçbir şeyin kıyaslaması yapılmadı. Rastgele duyarlıklı ondalıklar üzerinde ağaç yürüyen bir yorumlayıcı yapısı gereği yavaştır ve tasarım bunu açıkça kabul ediyor — paywall'lar oturum başına yaklaşık bir kez değerlendiriliyor. Ölçmediğim için sayı da vermiyorum.

Read further Devamını oku