8 Derleme, Yürütme ve Hatalar

8 Derleme, Yürütme ve Hatalar

Her ne kadar Lua'a yorumlanan bir dil desek de Lua, çalıştırmadan önce kaynak kodu daima bir ara forma(baytkod) getiren önderleme yapar. (Bu büyük bir olay değil: birçok yorumlanan dil aynı şeyi yapar.). Bir derleme aşamasının varlığı Lua gibi yorumlanan bir dilde kulağa garip gelebilir. Bununla birlikte yorumlanan dillerin ayırt edici özelliği derlenmedikleri değil derleyicinin çalışma zamanı dilin bir parçası olması ve bu nedenle anında oluşturulan kodu(baytkod) çalıştırma imkanıdır (ve kolaylığıdır). dofile gibi bir fonksiyonun varlığı Lua'nın yorumlanan dil olarak adlandırılmasına imkan veren şey olduğunu söyleyebiliriz.


8.1 Derleme

Daha önce dofile'ı Lua kod öbeklerini çalıştırmak için olan  basit bir tür işlem olarak tanıttık ancak dofile aslında yardımcı bir fonksiyondur: Asıl işi yapan loadfile. loadfile, dofile gibi bir dosyadan bir Lua öbeği yükler ancak öbeği çalıştırmaz. Bunun yerine yalnızca öbeği derler ve derlenmiş öbeği bir fonksiyon olarak döndürür. Ayrıca loadfile, dofile aksine hatalar yükseltmez ancak bunun yerine hata kodlarını döndürür böylece hatayı ele alabiliriz. Aşağıdaki gibi dofile'ı tanımlayabiliriz:

function dofile (dosyaismi)
       local f = assert(loadfile(dosyaismi))
       return f()
end

loadfile başarısız olduğunda hata yükseltilmesi için assert kullanımını unutmayın.

Basit görevler için dofile kullanışlıdır çünkü tek bir çağrıda bütün işi yapar. Ancak loadfile daha esnektir. Hata durumunda loadfile hatayı özelleştirilmiş yöntemlerle ele almamızı sağlayan nil artı hata iletisini döndürür. Ayrıca bir dosyayı birkaç kez çalıştırmamız gerekiyorsa loadfile'i bir kez çağırır ve sonuçlarını birkaç kez çağırabilirsiniz. Bu, dofile için yapılacak birkaç çağrıdan çok daha az maliyetlidir çünkü dosya yalnızca bir kez derlenir.

loadstring fonksiyonu öbeği bir dosyadan değilde bir stringden  okuması dışında dofile'a benzer. Örneğin şu koddan sonra

f = loadstring("i = i + 1")

f, çağrıldığında i = i + 1'i yürüten bir fonksiyon olacaktır:

i = 0
f(); print(i)           --> 1
f(); print(i)           --> 2

loadstring fonksiyonu güçlüdür; dikkatli kullanmalıyız. Aynı zamanda masraflı bir fonksiyondur (alternatiflere kıyasla) ve anlaşılmaz kodla sonuçlanabilir. Kullanmadan önce sorunu çözmek için daha basit bir yol olmadığından emin olun.

Hızlı ve pis bir dostring yapmak istiyorsanız (yani bir öbeği yükleyip çalıştırmak) sonucu doğrudan loadstring'den çağırabilirsiniz:

loadstring(s)()

Ancak herhangi bir sözdizimi hatası varsa loadstring nil döndürür ve nihai hata iletisi “bir nil değeri çağırma girişimi " gibi bir şey olur. Daha net hata iletileri için assert kullanın:

assert(loadstring(s))()

Genellikle loadstring'i yavan bir string üzerinde kullanmak mantıklı değil. Örneğin şu kod

f = loadstring("i = i + 1")

kabaca şuna eşdeğerdir

f = function () i = i + 1 end

ancak ikinci kod çok daha hızlıdır çünkü çevreleyen öbek derlendiğinde o da onunla yalnızca bir kez derlenir. İlk kodda, loadstring için her çağrı yeni bir derleme içerir.

loadstring sözcüksel kapsam ile derlemediğinden önceki örnekte iki kod eşdeğer değildir. Farkı görmek için örneği biraz değiştirelim:

i = 32
local i = 0
f = loadstring("i = i + 1; print(i)")
g = function () i = i + 1; print(i) end
f()          --> 33
g()          --> 1

g fonksiyonu beklendiği gibi yerel i'yi manipüle eder ancak f global i'yi manipüle eder çünkü loadstring stringlerini daima global ortamda derler.

loadstring'in en tipik kullanımı dış kodu yani programınızın dışından gelen kod parçalarını çalıştırmaktır. Örneğin kullanıcı tarafından tanımlanan bir fonksiyonun grafiğini çizmek istiyorsun; kullanıcı fonksiyon kodunu girer ve onu değerlendirmek için loadstring kullanırsınız. loadstring'in bir öbek yani deyimler beklediğini unutmayın. Bir ifadeyi değerlendirmek istiyorsanız return ile öneklemelisiniz böylece verilen ifadenin değerini döndüren bir deyim elde edersiniz. Örneğe bakalım:

print "ifadenizi girin:"
local l = io.read()
local func = assert(loadstring("return " .. l))
print("ifadenizin değeri " .. func())

loadstring ile döndürülen fonksiyon normal bir fonksiyon olduğundan birkaç kez çağırabilirsiniz:

print "çizilecek fonksiyonunuzu girin ('x' değişkeni ile):"
local l = io.read()
local f = assert(loadstring("return " .. l))
for i=1,20 do
   x = i -- global 'x' (öbekten görünür)
   print(string.rep("*", f()))
end

(string.rep fonksiyonu  bir string'i belirtilen sayıda çoğaltır.)

Daha derine inersek, Lua'daki gerçek primitifin ne loadfile ne de loadstring olmadığını, load olduğunu öğreniriz. loadfile gibi bir dosyadan bir öbek veya loadstring gibi bir stringden okumak  yerine  load öbeğini elde etmek için çağırdığı bir okuyucu fonksiyon alır. Okuyucu fonksiyon parça parça öbek döndürür; load , öbeğin sonunu gösteren nil dönene kadar onu çağırır. Nadiren load kullanıyoruz; ana kullanımı, öbek bir dosyada olmadığında (örneğin, dinamik olarak oluşturulan veya başka bir kaynaktan okunan) ve belleğe rahatça sığacak kadar büyük olduğundadır (aksi halde loadstring'i kullanabilirdik).

Lua herhangi bir bağımsız öbeğe değişken sayıda argümanlı anonim bir fonksiyonun gövdesi olarak  davranır. Örneğin, loadstring("a = 1"), aşağıdaki ifadenin eşdeğerini döndürür:

function (...) a = 1 end

Diğer herhangi bir fonksiyon gibi öbekler yerel değişkenler bildirebilir:

f = loadstring("local a = 10; print(a + 20)")
f() --> 30

Bu özellikleri kullanarak, global değişken x kullanımını önlemek için çizim örneğimizi yeniden yazabiliriz:

print "enter function to be plotted (with variable ’x’):"
local l = io.read()
local f = assert(loadstring("local x = ...; return " .. l))
for i=1,20 do
     print(string.rep("*", f(i)))
end

yerel bir değişken olarak x'i bildirmek için öbeğin başına "local x =..." bildirimini ekliyoruz. Daha sonra, vararg ifadesinin (...) değeri olan i argümanıyla f fonksiyonunu çağırıyoruz.

load fonksiyonları asla hatalar yükseltmez. Her hangi türde hata durumunda nil artı bir hata iletisi döndürürler:

print(loadstring("i i"))
    --> nil [string "i i"]:1: ’=’ expected near ’i’

Dahası, bu fonksiyonların asla herhangi bir türde yan etkisi yoktur. Onlar sadece bir içsel sembollere öbeği derler ve sonucu anonim bir fonksiyon olarak döndürür. Yaygın bir hata, yüklenen öbeğin fonksiyonları tanımladığını varsaymaktır. Lua'da, fonksiyon tanımlamaları atamadır; bu nedenle
derleme zamanında değil çalışma zamanında yapılırlar. Örneğin devamdaki gibi foo.lua dosyamız olduğunu varsayalım:

function foo (x)
   print(x)
end

Daha sonra devamdaki komutu çalıştıralım

f = loadfile("foo.lua")

Bu komuttan sonra foo derlenik ancak henüz tanımlı değil. Tanımlamak için öbeği çalıştırmalısınız:

print(foo)         --> nil
f()                -- 'foo' tanımlanıyor
foo("ok")          --> ok

Harici kod çalıştırması gereken ürün-kalitesinde bir programda bir öbek yüklerken bildirilen hataları yönetmelisiniz. Ayrıca kod güvenilir değilse kodu çalıştırırken hoş olmayan yan etkileri önlemek için korumalı bir ortamda ilgili yeni öbeği çalıştırmak isteyebilirsiniz.

8.2 C Kodu

Lua ile yazılan kodun aksine, C kodu kullanılmadan önce bir uygulama ile linklenmesi gerekir.
En popüler sistemlerde bu linklemeyi yapmanın en kolay yolu dinamik linkleme aracıdır.
Bununla birlikte bu araç ANSI C spesifikasyonunun bir parçası değildir; yani, bunu uygulamak için portatif-taşınabilir bir yol yoktur.

Normalde, Lua, ANSI C'de uygulanamayan herhangi bir imkanı içermez. ancak, dinamik linkleme farklıdır. Diğer tüm imkanların anası olarak görebilirsiniz onu: ona sahip olunca  Lua'da olmayan diğer imkanları dinamik olarak yükleyebiliriz. Bu nedenle, bu özel durumda, Lua taşınabilirlik kuralını kırar ve çeşitli platformlar için dinamik linkleme imkanını uygular. Standart uygulama, Windows, Mac OS X, Linux, FreeBSD, Solaris ve diğer bazı Unix uyarlamaları için bu desteği sağlar. Bu tesisi diğer platformlara genişletmek zor değil; Dağıtımınızı kontrol edin. (Bunu kontrol etmek için Lua istemcisinde print(package.loadlib("a","b")) çalıştırın ve sonucu görün. Dosyanın varolmamasından şikayet ederse dinamik linkleme imkanınız vardır. Aksi takdirde, hata iletisi, bu imkanın desteklenmediğini veya yüklenmediğini gösterir.)

Lua, package_loadlib adlı tek bir fonksiyonla dinamik linklemenin tüm işlevselliğini sağlar. İki string argümana sahiptir: kütüphanenin tam yolu ve bir fonksiyon adı. Mamafi, tipik bir çağrı şuna benzer:

local path = "/usr/local/lib/lua/5.1/socket.so"
local f = package.loadlib(path, "luaopen_socket")

loadlib fonksiyonu verilen kütüphaneyi yükler ve onu Lua'a linkler. Ancak fonksiyonu çağırmaz.
Bunun yerine C fonksiyonunu bir Lua fonksiyonu olarak döndürür. kütüphanenin yüklenmesi veya ilkleme fonksiyonunu bulmada herhangi bir hata varsa loadlib nil artı bir hata iletisi döndürür.

loadlib fonksiyonu oldukça düşük seviyeli bir fonksiyondur. Kütüphanenin tam yolunu ve fonksiyonun adını doğru (derleyici tarafından eklenen önde bulunan alt çizgi de dahil olmak üzere) sağlamalıyız. Genellikle, C kütüphanelerini require kullanarak yükleriz. Bu fonksiyon kütüphaneyi arar ve kütüphanenin ilkleme fonksiyonunu yüklemek için loadlib 'i kullanır. bir kez çağırmayla, bu ilkleme fonksiyonu bu kütüphanedeki fonksiyonları Lua'da kaydeder, diğer fonksiyonların tanımlandığı tipik bir Lua öbeği gibi. 26.2 Bölümünde C kütüphaneleri 15.1 Bölümünde require hakkında daha fazla ayrıntıya gireceğiz.

8.3 Hatalar

Errare humanum est(hata insanidir). Bu nedenle hataları elimizden gelenin en iyisiyle ele almalıyız. Lua sık sık bir uygulamaya gömülü bir eklenti dil olarak kullanıldığından bir hata olduğunda bildik çöküş veya çıkışı yaptırmaz. Bunun yerine bir hata oluştuğunda Lua geçerli öbeği sonlar ve uygulamaya döner.

Lua beklenmeyen bir durumla karşılaştığında bir hata yükseltir. Hatalar, siz (yani programınız), sayı olmayan değerle matematiksel işlem, fonksiyon olmayan değerleri çağırma, tablo olmayan değerleri indeksleme (Daha sonra göreceğimiz üzere bu davranışı meta tablolar kullanarak değiştirebilirsiniz.) vb. denediğinizde ortaya çıkar. argüman olarak hata iletisiyle error fonksiyonunu çağırarak bir hatayı yükseltebilirsiniz. Genellikle bu fonksiyon kodunuzdaki hataları işlemek için uygun yoldur:

print "bir sayı girin:"
n = io.read("*number")
if not n then error("geçersiz girdi") end

if not koşul then error end  kombinasyonu o kadar yaygındır ki Lua, assert ismiyle bu iş için tümleşik bir fonksiyona sahiptir:

print "bir sayı girin:"
n = assert(io.read("*number"), "geçersiz girdi")

assert fonksiyonu ilk argümanının false olup olmadığını denetler ve bu argümanı basitçe döndürür;
argüman false ise (false veya nil) assert bir hata yükseltir. İkinci argümanı, mesaj, isteğe bağlıdır. Bununla birlikte bu assert normal bildik bir fonksiyondur. Yani Lua, fonksiyonu çağırmadan önce argümanlarını her zaman değerlendirir. Eğer bunun gibi bir şeye sahipseniz

n = io.read()
assert(tonumber(n), "geçersiz girdi: " .. n .. " bir sayı değil")

Lua, n bir sayı olsa bile daima birleştirmeyi yapacaktır. Bu gibi durumlarda açık bir test kullanmak akıllıca olabilir.

Bir fonksiyon beklenmedik bir durum (bir istisna) bulduğunda iki temel davranış sergileyebilir: bir hata kodu (genellikle nil) döndürebilir veya error fonksiyonunu çağırarak bir hata yükseltebilir. Bu iki seçenek arasında seçim yapmak için sabit kurallar yok ancak genel bir ilke edinebiliriz: kolayca kaçınılabilecek bir istisna, hata yükseltmeli; aksi takdirde bir hata kodu döndürmelidir.

Örneğin sin fonksiyonunu düşünelim. Bir tablo çağırdığında nasıl davranmalı? Bir hata kodu döndürdüğünü varsayalım. Hataları kontrol etmemiz gerekiyorsa şöyle bir şey yazmamız gerekir

local res = math.sin(x)
if not res then                    -- hata?
    <hata işleme kodu>


Ancak fonksiyonu çağırmadan önce bu özel durumu kolayca kontrol edebiliriz:

if not tonumber(x) then           -- x bir sayı değil mi?
    <hata işleme kodu>

Sık sık , ne sin çağrısının sonucunu ne de argümanını kontrol ediyoruz; argüman bir sayı değilse muhtemelen programımıza yanlış bir şey diyecektir. Bu gibi durumlarda, hesaplamayı durdurmak ve bir hata mesajı vermek, istisnayı ele almanın en basit ve en pratik yoludur. 

Öte yandan, bir dosya açan io.open fonksiyonunu düşünelim. Olmayan bir dosyayı okumak için çağrıldığında nasıl davranmalı? Bu durumda, fonksiyonu çağırmadan önce istisnayı kontrol etmenin basit bir yolu yoktur. Birçok sistemde bir dosyanın var olup olmadığını bilmenin tek yolu onu açmaya çalışmak. Bu nedenle, io.open harici bir nedenden dolayı (“dosya yok” veya “izin reddedildi " gibi) bir dosyayı açamıyorsa, nil ve hata mesajı olan bir string döndürür. Bu şekilde, örneğin kullanıcıya başka bir dosya adı sorarak durumu uygun bir şekilde işleme şansınız vardır:

local file, msg
repeat
    print "dosya ismini girin:"
    local name = io.read()
    if not name then return end          -- girdi yok
    file, msg = io.open(name, "r")
    if not file then print(msg) end
until file

Bu tür durumları ele almak istemiyorsanız ancak yine de güvenli oynamak istiyorsanız işlemi korumak için basitçe assert'i kullanın:

file = assert(io.open(name, "r"))

Bu tipik bir Lua deyişi: io.open başarısız olursa assert bir hata yükseltecek.

file = assert(io.open("no-file", "r"))
     --> stdin:1: no-file: No such file or directory

io.open'den ikinci sonuç olan hata mesajının assert 'e ikinci argüman olarak nasıl gittiğine dikkat.

8.4 Hata İşleme ve İstisnalar

Birçok uygulama için Lua'da herhangi hata işleme yapmanız gerekmez; uygulama programı bu işi yapar. Tüm Lua faaliyetleri genellikle bir öbeği çalıştırmak için Lua'a başvuran uygulama tarafından yapılan bir çağrıyla başlar. Herhangi bir hata varsa bu çağrı bir hata kodu döndürür böylece uygulama uygun eylemler yapabilir. Bağımsız yorumlayıcı(lua.exe) kullanıldığı durumda ana döngüsü sadece hata mesajını yazdırır ve komut istemini göstermeye ve komutları çalıştırmaya devam eder.

Lua'da  hataları işlemeye gereksiniyorsan kodunuzu kapsüllemek için pcall fonksiyonunu (korumalı çağrı) kullanmanız gerekir.

Lua kodunun bir parçasını çalıştırmak ve bu kodu çalıştırırken ortaya çıkan herhangi bir hatayı yakalamak istediğinizi varsayalım. İlk adımınız, bu kod parçasını bir fonksiyonda kapsüllemektir; buna foo diyelim:

function foo ()
    <bazı kodlar>
    if beklenmedik_durum then error() end
    <bazı kodlar>
    print(a[i]) -- potansiyel hata ’a’ bir tablo değil
    <bazı kodlar>
end

sonra foo'u pcall ile çağır:

if pcall(foo) then
    --’foo’ yürütülürken hata yok
    <normal kod>
else
    -- ’foo’ bir hata yükseltti:uygun eylemleri yap
    <hata işleme kodu>
end

Elbette, pcall'ı anonim bir fonksiyonla çağırabilirsiniz:

if pcall(function ()
   <korumalı kod>
end) then
    <normal kod>
else
    <hata işleme kodu>
end

pcall fonksiyonu ilk argümanını korumalı modda çağırır bu şekilde fonksiyon yürütülürken tüm hataları yakalar. Hata yoksa pcall true, artı çağrı tarafından döndürülen tüm değerleri döndürür. Aksi takdirde false, artı hata iletisi döndürür.

Adına rağmen, hata iletisinin bir string olması gerekmez.  error'e geçeceğiniz herhangi bir Lua değeri pcall ile döndürülecektir:

local status, err = pcall(function () error({code=121}) end)
print(err.code) --> 121

Bu mekanizmalar Lua 'da istisna işlemek için gereksindiğiniz herşeyi sağlar. error ile istisna fırlatır(throw) ve pcall ile yakalarız(catch). Hata mesajı, hatayı veya türünü tanımlar.

8.5 Hata mesajları ve geriiz

Her ne kadar bir hata iletisi olarak herhangi bir tür değeri kullanabilseniz de genellikle hata iletileri neyin yanlış gittiğini açıklayan stringlerdir. Bir içsel hata olduğunda (tablo olmayan değeri indeksleme girişimi gibi) Lua hata iletisi üretir; aksi takdirde, hata iletisi error fonksiyonuna geçilen değerdir. Mesaj bir string olduğunda Lua hatanın gerçekleştiği yer hakkında bazı bilgiler eklemeye çalışır:

local status, err = pcall(function () a = "a"+1 end)
print(err)
    --> stdin:1: attempt to perform arithmetic on a string value

local status, err = pcall(function () error("my error") end)
print(err)
     --> stdin:1: my error

konum bilgisi, dosya ismi(örneğimizde stdin-standart girdi) artı satır numarasını(örneğimizde 1) verir.

error fonksiyonu, hatayı rapor etmesi gereken seviyeyi veren ikinci bir ek parametreye sahiptir; bu parametreyi hata için başka birini suçlamak için kullanabilirsiniz. Örneğin, ilk görevi doğru çağrılıp çağrılmadığını kontrol etmek olan bir fonksiyon yazdığınızı varsayalım:

function foo (str)
    if type(str) ~= "string" then
         error("string beklenmişti")
    end
    <normal kod>
end

Ardından, birisi fonksiyonunuzu yanlış bir argüman ile çağırsın:

foo({x=1})

Olacak olan, Lua parmağıyla fonksiyonunuzu işaret eder - error'u çağıran foo olmasına karşın — ve gerçek suçlu değil, çağıran. Bu sorunu gidermek için raporladığınız hatanın çağrı hiyerarşisinde 2. seviyede meydana geldiğini bildirirsiniz.(Seviye 1 kendi fonksiyonunuz):

function foo (str)
    if type(str) ~= "string" then
        error("string bekleniyordu", 2)
    end
    <normal kod>
end

Bir hata olduğu zaman sadece hatanın oluştuğu yerden daha fazla hata ayıklama bilgisi isteriz sık sık. En azından, hataya yönlendiren çağrıların tüm yığınını gösteren bir geriiz isteriz. pcall hata mesajını döndürdüğünde yığının bir kısmını yok eder (ondan hata noktasına giden kısmı). Sonuç olarak bir geriiz istiyorsanız pcall dönmeden önce onu oluşturmamız gerekir. Bunu yapmak için Lua xpcall fonksiyonunu sağlar. Çağrılacak fonksiyonun yanı sıra ikinci bir argüman olarak bir hata işleyicisi fonksiyon alır. Hata durumunda Lua bu hata işleyicisini yığın çözülmeden önce çağırır böylece hata hakkında istediği ekstra bilgileri toplamak için debug kütüphanesini kullanabilir. iki yaygın hata işleyicisinden debug.debug, size hata olduğunda neler olup bittiğini kendiniz inceleyebileceğiniz Lua istemi veren; ve bir geriiz ile genişletilmiş bir hata iletisi oluşturan debug.traceback. (Daha sonra hata ayıklama kütüphanesini ele aldığımızda bu fonksiyonlar hakkında daha fazla şey göreceğiz) . İkincisi, bağımsız yorumlayıcının hata mesajlarını oluşturmak için kullandığı fonksiyondur. Ayrıca herhangi bir zamanda geçerli yürütmede bir geriiz almak için  debug.traceback'i çağırabilirsiniz:

print(debug.traceback())

Hiç yorum yok:

Yorum Gönder