Zephyrnet Logosu

Luau'da Semantik Alt Tipleme

Tarih:

Luau, semantik alt tiplemenin gücünü milyonlarca yaratıcının ellerine teslim eden ilk programlama dilidir.

Yanlış pozitifleri en aza indirme

Roblox Studio'daki Komut Dosyası Analizi widget'ı gibi araçlarda tür hatası raporlamayla ilgili sorunlardan biri şudur: yanlış pozitif. Bunlar, analizin ürünü olan uyarılardır ve çalışma zamanında oluşabilecek hatalara karşılık gelmez. Örneğin, program

local x = CFrame.new() local y if (math.random()) o zaman y = CFrame.new() else y = Vector3.new() end local z = x * y

çalışma zamanında gerçekleşemeyecek bir tür hatası bildirir, çünkü CFrame her ikisi ile çarpmayı destekler Vector3 ve CFrame. (Türü ((CFrame, CFrame) -> CFrame) & ((CFrame, Vector3) -> Vector3).)

Yanlış pozitifler, yeni kullanıcıları işe almak için özellikle zayıftır. Yazım meraklısı bir içerik oluşturucu, dizgi denetimini açarsa ve hemen sahte kırmızı dalgalı çizgilerle dolu bir duvarla karşılaşırsa, onu hemen tekrar kapatmak için güçlü bir teşvik vardır.

Bir çalışma zamanı hatasının tetiklenip tetiklenmeyeceğine önceden karar vermek imkansız olduğundan, yazım hatalarındaki yanlışlıklar kaçınılmazdır. Tip sistemi tasarımcıları, yanlış pozitiflerle mi yoksa yanlış negatiflerle mi yaşayacaklarını seçmek zorundadır. Luau'da bu, mod tarafından belirlenir: strict mod, yanlış pozitifler tarafında hata yapar ve nonstrict mod yanlış negatifler tarafında hata yapar.

Hatalar kaçınılmaz olmakla birlikte, sahte hatalara ve otomatik tamamlama veya API dokümantasyonu gibi türe dayalı araçlarda belirsizliğe neden olduklarından, mümkün olduğunda bunları kaldırmaya çalışıyoruz.

Yanlış pozitiflerin kaynağı olarak alt tipleme

Luau'daki (ve TypeScript veya Flow gibi diğer birçok benzer dildeki) yanlış pozitiflerin kaynaklarından biri şudur: subtiplemesi. Alt tipleme, bir değişken başlatıldığında veya atandığında ve bir işlev çağrıldığında kullanılır: tip sistemi, ifade tipinin değişken tipinin bir alt tipi olup olmadığını kontrol eder. Örneğin yukarıdaki programa türleri eklersek

yerel x : CFrame = CFrame.new() yerel y : Vector3 | CFrame eğer (math.random()) ise y = CFrame.new() else y = Vector3.new() end local z : Vector3 | CFrame = x * y

daha sonra tip sistemi, tipin tipini kontrol eder. CFrame çarpma bir alt türüdür (CFrame, Vector3 | CFrame) -> (Vector3 | CFrame).

Alt tipleme çok kullanışlı bir özelliktir ve type union () gibi zengin tip yapıları destekler.T | U) ve kesişme (T & U). Örneğin, number? bir birlik türü olarak uygulanır (number | nil), sayı veya sayı olan değerlerin yaşadığı nil.

Ne yazık ki, alt tiplemenin kesişim ve birleşim tipleriyle etkileşimi garip sonuçlara sahip olabilir. Eski Luau'daki basit (ama oldukça yapay) bir durum şuydu:

local x : (number?) & (string?) = nil local y : nil = nil y = x -- '(number?) & (string?)' türü 'nil'e dönüştürülemedi x = y

Bu hata, alt tiplemedeki bir hatadan kaynaklanır, eski alt tipleme algoritması şunu bildirir: (number?) & (string?) bir alt türü değildir nil. Bu yanlış bir pozitiftir, çünkü number & string ıssızdır, bu nedenle olası tek sakini (number?) & (string?) is nil.

Bu yapay bir örnek, ancak sorunlardan kaynaklanan yaratıcılar tarafından ortaya atılan gerçek sorunlar var, örneğin https://devforum.roblox.com/t/luau-recap-july-2021/1382101/5. Şu anda, bu sorunlar çoğunlukla gelişmiş yazı sistemi özelliklerinden yararlanan içerik oluşturucuları etkiliyor, ancak yazı çıkarımını daha doğru hale getirdikçe, birleşim ve kesişim türleri, tür ek açıklamaları olmayan kodlarda bile daha yaygın hale gelecek.

Eski yaklaşımımızdan uzaklaştığımız için, bu tür yanlış pozitifler artık Luau'da görülmemektedir. sözdizimsel alt tipleme denilen bir alternatife anlamsal alt tipleme.

sözdizimsel alt tipleme

AKA "daha önce yaptığımız şey."

Sözdizimsel alt tipleme, sözdizimine yönelik özyinelemeli bir algoritmadır. Kesişim ve birleşim türleriyle ilgili ilginç durumlar şunlardır:

  • yansıma: T bir alt türü T
  • L Kavşağı: (T₁ & … & Tⱼ) bir alt türü U ne zaman bazı Tᵢ alt türleri U
  • Birlik L: (T₁ | … | Tⱼ) bir alt türü U ne zaman hepsi Tᵢ alt türleri U
  • R kavşağı: T bir alt türü (U₁ & … & Uⱼ) her ne zaman T hepsinin bir alt tipidir Uᵢ
  • Birlik R: T bir alt türü (U₁ | … | Uⱼ) her ne zaman T bazılarının bir alt tipidir. Uᵢ.

Örneğin:

  • Yansıma ile: nil bir alt türü nil
  • yani Union R tarafından: nil bir alt türü number?
  • ve: nil bir alt türü string?
  • R Kavşağı'na göre: nil bir alt türü (number?) & (string?).

Yay! Ne yazık ki, bu kuralları kullanarak:

  • number bir alt türü değil nil
  • yani Birlik L tarafından: (number?) bir alt türü değil nil
  • ve: string bir alt türü değil nil
  • yani Birlik L tarafından: (string?) bir alt türü değil nil
  • L Kavşağı ile: (number?) & (string?) bir alt türü değil nil.

Bu, sözdizimsel alt tiplemenin tipik bir örneğidir: "evet" sonucu döndürdüğünde doğrudur, ancak "hayır" sonucu döndürdüğünde yanlış olabilir. Algoritma bir konservatif yaklaşımve "hayır" sonucu yazım hatalarına yol açabileceğinden, bu yanlış pozitiflerin kaynağıdır.

Anlamsal alt tipleme

AKA "şimdi ne yapıyoruz?"

Alt tiplemeyi sözdizimine yönelik olarak düşünmek yerine, önce anlambilimini ele alıyoruz ve sonra anlambilimin nasıl uygulandığına dönüyoruz. Bunun için anlamsal alt tiplemeyi benimsiyoruz:

  • Bir türün semantiği bir değerler kümesidir.
  • Kavşak türleri kümelerin kesişimleri olarak düşünülür.
  • Birleşim türleri, kümelerin birleşimleri olarak düşünülür.
  • Alt tipleme, küme dahil etme olarak düşünülür.

Örneğin:

Tip Anlambilim
number { 1, 2, 3, … }
string { “foo”, “çubuk”,… }
nil { sıfır }
number? { sıfır, 1, 2, 3, … }
string? { sıfır, "foo", "bar", … }
(number?) & (string?) { sıfır, 1, 2, 3, … } ∩ { sıfır, "foo", "bar", … } = { sıfır }

ve alt türler, küme kapanımları olarak yorumlandığından:

Alt tür süper tip Çünkü
nil number? { sıfır } ⊆ { sıfır, 1, 2, 3, … }
nil string? { sıfır } ⊆ { sıfır, "foo", "bar", … }
nil (number?) & (string?) { sıfır } ⊆ { sıfır }
(number?) & (string?) nil { sıfır } ⊆ { sıfır }

Anlamsal alt tiplemeye göre, (number?) & (string?) eşdeğerdir nil, ancak sözdizimsel alt tipleme yalnızca bir yönü destekler.

Bunların hepsi iyi ve güzel, ancak araçlarda anlamsal alt tiplemeyi kullanmak istiyorsak, bir algoritmaya ihtiyacımız var ve anlamsal alt tiplemeyi kontrol etmenin önemsiz olmadığı ortaya çıktı.

Anlamsal alt tipleme zordur

NP-kesin olması zor.

Grafiği bir Luau tipi olarak kodlayarak grafik renklendirmeyi anlamsal alt tiplemeye indirgeyebiliriz, öyle ki tipler üzerinde alt tiplemeyi kontrol etmek, grafiği renklendirmenin imkansızlığını kontrol etmekle aynı sonucu verir.

Örneğin, üç düğümlü, iki renkli bir grafiğin renklendirilmesi, türler kullanılarak yapılabilir:

tür Kırmızı = "kırmızı" tür Mavi = "mavi" tür Renk = Kırmızı | Mavi tip Renklendirme = (Renk) -> (Renk) -> (Renk) -> boole tipi Renksiz = (Renk) -> (Renk) -> (Renk) -> yanlış

Daha sonra bir grafik, alt tip ile bir aşırı yük fonksiyon tipi olarak kodlanabilir. Uncolorable ve süper tip Coloringdöndüren aşırı yüklenmiş bir işlev olarak false bir kısıtlama ihlal edildiğinde. Her aşırı yük bir kısıtlamayı kodlar. Örneğin, bir çizgi, bitişik düğümlerin aynı renge sahip olamayacağını söyleyen kısıtlamalara sahiptir:

yazın Çizgi = Renklendirme & ((Kırmızı) -> (Kırmızı) -> (Renk) -> yanlış) & ((Mavi) -> (Mavi) -> (Renk) -> yanlış) & ((Renk) -> () Kırmızı) -> (Kırmızı) -> yanlış) & ((Renk) -> (Mavi) -> (Mavi) -> yanlış)

Bir üçgen benzerdir, ancak bitiş noktaları da aynı renge sahip olamaz:

tür Üçgen = Çizgi & ((Kırmızı) -> (Renk) -> (Kırmızı) -> yanlış) & ((Mavi) -> (Renk) -> (Mavi) -> yanlış)

Şimdi, Triangle bir alt türü Uncolorable, fakat Line değildir, çünkü çizgi 2 renkli olabilir. Bu, herhangi bir sonlu sayıda renge sahip herhangi bir sonlu grafik için genelleştirilebilir ve bu nedenle alt tip kontrolü NP-zordur.

Bununla iki şekilde başa çıkıyoruz:

  • bellek ayak izini azaltmak için türleri önbelleğe alırız ve
  • türlerin önbelleği çok büyürse "Kod Çok Karmaşık" hatasıyla pes edin.

Umarım bu pratikte pek gündeme gelmez. Bunun gibi sorunların pratikte Standard ML'deki gibi tip sistemlerle ilgili deneyimlerden kaynaklanmadığına dair iyi kanıtlar vardır. EXPTIME-tamamlandı, ancak pratikte Turing Makinesi şeritlerini türler olarak kodlamak için kendi yolunuzdan çıkmanız gerekir.

Tip normalleştirme

Anlamsal alt tiplemeye karar vermek için kullanılan algoritma tip normalleştirme. Sözdizimi tarafından yönlendirilmek yerine, önce türleri normalleştirilecek şekilde yeniden yazarız, ardından normalleştirilmiş türlerde alt tiplemeyi kontrol ederiz.

Normalleştirilmiş bir tür, aşağıdakilerin birleşimidir:

  • normalleştirilmiş sıfır türü (ya never or nil)
  • normalleştirilmiş bir sayı türü (ya never or number)
  • normalleştirilmiş bir boole türü (ya never or true or false or boolean)
  • normalleştirilmiş bir işlev türü (ya never veya fonksiyon türlerinin kesişimi) vb.

Tipler normalleştirildikten sonra anlamsal alt tiplemeyi kontrol etmek kolaydır.

Her tür normalleştirilebilir (genel tür paketleriyle ilgili bazı teknik kısıtlamalarla birlikte). Önemli adımlar şunlardır:

  • eşleşmeyen ilkellerin kesişme noktalarını kaldırma, örn. number & bool tarafından değiştirilir never, ve
  • fonksiyon birliklerinin kaldırılması, örn. ((number?) -> number) | ((string?) -> string) tarafından değiştirilir (nil) -> (number | string).

Örneğin, normalleştirme (number?) & (string?) kaldırır number & string, yani geriye kalan tek şey nil.

Tip normalleştirmeyi uygulamaya yönelik ilk girişimimiz bunu özgürce uyguladı, ancak bu korkunç bir performansla sonuçlandı (karmaşık kod, bir dakikadan kısa bir süre içinde tip kontrolünden bir gecede çalışmaya başladı). Bunun nedeni rahatsız edici derecede basit: Luau'nun alt tipleme algoritmasında refleksiviteyi işlemek için bir optimizasyon var (T bir alt türü T) ucuz bir işaretçi eşitlik kontrolü gerçekleştirir. Tip normalleştirme, işaretçiyle özdeş türleri, performansı önemli ölçüde düşüren anlamsal olarak eşdeğer (ancak işaretçiyle özdeş olmayan) türlere dönüştürebilir.

Bu performans sorunları nedeniyle, alt tipleme için ilk kontrolümüz olarak hala sözdizimsel alt tiplemeyi kullanıyoruz ve sadece sözdizimsel algoritma başarısız olursa tip normalleştirmesi gerçekleştiriyoruz. Bu doğru, çünkü sözdizimsel alt tipleme anlamsal alt tiplemeye muhafazakar bir yaklaşımdır.

Pragmatik anlamsal alt tipleme

Kullanıma hazır semantik alt tipleme, Luau'da uygulanandan biraz farklıdır, çünkü modellerin set-teorik, bu da işlev türlerinin sakinlerinin "işlevler gibi davranmasını" gerektirir. Bu şartı kaldırmamızın iki nedeni var.

Ilk olarak, işlev türlerini işlevlerin bir kesişimine göre normalleştiririz, örneğin korkunç bir birleşimler karmaşası ve işlevlerin kesişimleri:

((sayı?) -> sayı?) | (((sayı) -> sayı) & ((dize?) -> dizi?))

aşırı yüklenmiş bir işleve normalleştirir:

((sayı) -> sayı?) & ((sıfır) -> (sayı | dizi)?)

Küme-teorik semantik alt tipleme bu normalleştirmeyi desteklemez ve bunun yerine işlevleri şu şekilde normalleştirir: ayrık normal form (işlevlerin kesişim birlikleri). Bunu ergonomik nedenlerle yapmıyoruz: Luau'da aşırı yüklenmiş işlevler deyimseldir, ancak DNF değildir ve kullanıcılara bu tür deyimsel olmayan türler sunmak istemiyoruz.

Normalleştirmemiz, işlev türlerinin birliklerini yeniden yazmaya dayanır:

((A) -> B) | ((C) -> D) → (A & C) -> (B | D)

Bu normalleştirme bizim modelimizde geçerli ama küme-teorik modellerde değil.

Ikinci olarak, Luau'da bir işlev uygulamasının türü f(x) is B if f türü var (A) -> B ve x türü var A. Beklenmedik bir şekilde, bu, ıssız türler nedeniyle küme-teorik modellerde her zaman doğru değildir. Küme-teorik modellerde, eğer x türü var never sonra f(x) türü var never. Özellikle bu köşe durumu yalnızca ölü kodda ortaya çıkabileceğinden, kullanıcılara işlev uygulamasının özel bir köşe durumu olduğu fikrini yüklemek istemiyoruz.

Küme-teorik modellerde, (never) -> A bir alt türü (never) -> B, ne olursa olsun A ve B vardır. Luau'da bu doğru değil.

Bu iki nedenden dolayı (teknik herhangi bir şeyden ziyade büyük ölçüde ergonomi ile ilgilidir) küme-teorik gerekliliği bırakıyoruz ve pragmatik anlamsal alt tipleme.

olumsuzlama türleri

Luau'nun tip sistemi ile kullanıma hazır semantik alt tipleme arasındaki diğer fark, Luau'nun olumsuzlanan tüm tipleri desteklememesidir.

Olumsuzlanan türler istemek için yaygın durum, tip denetimi koşullarıdır:

-- başlangıçta x'in türü T if (type(x) == "string") sonra -- bu dalda x'in türü T ve string else -- bu dalda x'in türü T & ~string end

Bu olumsuzlanmış bir tür kullanır ~string dize olmayan değerlerin yaşadığı.

Luau'da, yalnızca bu tür yazım düzeltmelerine izin veririz. test tipleri sevmek stringfunctionPart ve benzeri ve değil gibi yapısal tiplerde (A) -> B, genel olumsuzlanan türlerin yaygın durumundan kaçınır.

Prototip oluşturma ve doğrulama

Luau'nun semantik alt tipleme algoritmasının tasarımı sırasında değişiklikler yapıldı (örneğin, başlangıçta set-teorik alt tiplemeyi kullanabileceğimizi düşündük). Bu hızlı değişim döneminde, hızlı bir şekilde yineleme yapabilmek önemliydi, bu nedenle başlangıçta bir prototip doğrudan bir üretim uygulamasına atlamak yerine.

Alt tipleme algoritmaları beklenmedik köşe durumlarına sahip olabileceğinden, prototipin doğrulanması önemliydi. Bu nedenle prototipleme dili olarak Agda'yı benimsedik. Agda, birim testini desteklemenin yanı sıra mekanize doğrulamayı da destekler, bu nedenle tasarıma güveniyoruz.

Prototip, Luau'nun tamamını değil, yalnızca işlevsel alt kümesini uygular, ancak bu, muhtemelen üretimde düzeltilmesi zor hatalar olarak ortaya çıkacak olan ince özellik etkileşimlerini keşfetmek için yeterliydi.

Prototipleme mükemmel değildir, örneğin üretimde karşılaştığımız ana sorunlar, bir prototip tarafından asla yakalanamayacak olan performans ve C++ standart kitaplığı ile ilgiliydi. Ancak üretim uygulaması bunun dışında oldukça basitti (veya en azından bir 3kLOC değişikliğinin olabileceği kadar basit).

Sonraki adımlar

Semantik alt tipleme, bir yanlış pozitif kaynağı ortadan kaldırdı, ancak hâlâ izini sürmemiz gereken başka kaynaklar da var:

  • Aşırı yüklenmiş fonksiyon uygulamaları ve operatörler
  • Karmaşık türdeki ifadelerde özellik erişimi
  • Tabloların salt okunur özellikleri
  • Zaman içinde türü değiştiren değişkenler (aka tür durumları)

Sahte kırmızı dalgalı çizgileri kaldırma arayışı devam ediyor!

Teşekkürler

Giuseppe Castagna ve Ben Greenman'a bu yazının taslakları hakkında faydalı yorumları için teşekkürler.


Alan, Roblox Studio'daki geliştirme özelliklerinin birçoğunu yönlendirmeye yardımcı olan Luau tipi sistemin tasarımını ve uygulamasını koordine eder. Jeffrey, programlama dillerinde 30 yılı aşkın araştırma deneyimine sahiptir, çok sayıda açık kaynaklı yazılım projesinin aktif bir üyesi olmuştur ve İngiltere, Oxford Üniversitesi'nden bir DPhil'e sahiptir.

spot_img

En Son İstihbarat

spot_img