Stork, Bölüm 4: İfadelerin Uygulanması ve Sonuçlandırılması
Yayınlanan: 2022-03-11C++ kullanarak hafif bir programlama dili oluşturma arayışımızda, üç hafta önce tokenizer'ımızı oluşturarak başladık ve sonraki iki hafta içinde ifade değerlendirmesini uyguladık.
Şimdi, olgun bir programlama dili kadar güçlü olmayacak, ancak çok küçük bir ayak izi de dahil olmak üzere gerekli tüm özelliklere sahip olacak eksiksiz bir programlama dilini tamamlama ve sunma zamanı.
Yeni şirketlerin web sitelerinde sıkça sorulan sorulara değil de sorulmasını istedikleri sorulara cevap veren SSS bölümlerinin olması bana komik geliyor. Aynısını burada da yapacağım. Çalışmamı izleyen insanlar genellikle bana Stork'un neden bazı bayt kodlarını veya en azından bazı ara dilleri derlemediğini soruyor.
Stork Neden Bytecode'a Derleme Yapmıyor?
Bu soruyu cevaplamaktan mutluluk duyarım. Amacım, C++ ile kolayca entegre olacak, az yer kaplayan bir betik dili geliştirmekti. “Küçük ayak izi” için kesin bir tanımım yok, ancak daha az güçlü cihazlara taşınabilirliği sağlayacak kadar küçük olacak ve çalıştırıldığında çok fazla bellek tüketmeyecek bir derleyici hayal ediyorum.
Hıza odaklanmadım, çünkü zaman açısından kritik bir göreviniz varsa C++ ile kod yazacağınızı düşünüyorum, ancak bir çeşit genişletilebilirliğe ihtiyacınız varsa, o zaman Stork gibi bir dil faydalı olabilir.
Benzer bir görevi yerine getirebilecek başka, daha iyi diller olmadığını iddia etmiyorum (örneğin, Lua). Onlar olmasaydı gerçekten trajik olurdu ve ben size sadece bu dilin kullanım durumu hakkında bir fikir veriyorum.
C++ içine gömüleceği için, benzer bir amaca hizmet edecek bir ekosistemin tamamını yazmak yerine, C++'ın bazı mevcut özelliklerini kullanmayı kullanışlı buluyorum. Sadece bu değil, aynı zamanda bu yaklaşımı daha ilginç buluyorum.
Her zaman olduğu gibi, kaynak kodunun tamamını GitHub sayfamda bulabilirsiniz. Şimdi, ilerlememize daha yakından bakalım.
Değişiklikler
Bu kısma kadar Stork kısmen tamamlanmış bir üründü, bu yüzden tüm dezavantajlarını ve kusurlarını göremedim. Ancak, daha eksiksiz bir şekil aldığı için önceki bölümlerde tanıtılan aşağıdaki şeyleri değiştirdim:
- Fonksiyonlar artık değişken değildir. Şimdi
compiler_context
ayrı birfunction_lookup
var.function_param_lookup
, karışıklığı önlemek içinparam_lookup
olarak yeniden adlandırıldı. - Fonksiyonların çağrılma şeklini değiştirdim.
call
argümanlarınstd::vector
runtime_context
alan, eski dönüş değeri indeksini depolayan, argümanları yığına iten, dönüş değeri indeksini değiştiren, fonksiyonu çağıran, yığından argümanları çıkaran, eski dönüş değeri indeksini geri yükleyen ve sonucu döndürür. Bu şekilde, C++ yığını bu amaca hizmet ettiğinden, daha önce olduğu gibi dönüş değeri dizinleri yığınını tutmamız gerekmez. -
compiler_context
eklenen ve üye işlevleriscope
vefunction
yapılan çağrılar tarafından döndürülen RAII sınıfları. Bu nesnelerin her biri, yapıcılarında sırasıyla yenilocal_identifier_lookup
veparam_identifier_lookup
oluşturur ve yıkıcıdaki eski durumu geri yükler. -
get_scope
üye işlevi tarafından döndürülenruntime_context
eklenen bir RAII sınıfı. Bu işlev, yığın boyutunu yapıcısında saklar ve onu yıkıcısında geri yükler. - Genel olarak
const
anahtar sözcüğünü ve sabit nesneleri kaldırdım. Yararlı olabilirler ama kesinlikle gerekli değildirler. -
var
anahtar kelimesi kaldırıldı, çünkü şu anda hiç gerekli değil. - Çalışma zamanında bir dizi boyutunu kontrol edecek
sizeof
anahtar sözcüğünü ekledim. Belki bazı C++ programcıları, C++sizeof
derleme zamanında çalıştığı için ad seçimini küfürlü bulabilir, ancak bu anahtar sözcüğü bazı yaygın değişken adlarıyla - örneğin,size
- çarpışmayı önlemek için seçtim. - Her şeyi açıkça
string
dönüştürentostring
anahtar sözcüğünü ekledim. İşlev aşırı yüklenmesine izin vermediğimiz için bir işlev olamaz. - Çeşitli daha az ilginç değişiklikler.
Sözdizimi
C ve ilgili programlama dillerine çok benzer bir sözdizimi kullandığımız için, size net olmayan detayları vereceğim.
Değişken tipi bildirimleri aşağıdaki gibidir:
-
void
, yalnızca işlev dönüş türü için kullanılır -
number
-
string
-
T[]
,T
türündeki öğeleri tutan bir dizidir. -
R(P1,...,Pn)
,R
tipini döndüren veP1
-Pn
arası argümanları alan bir fonksiyondur. Bu türlerin her biri, referansla iletilmişse,&
ile ön eklenebilir.
Fonksiyon bildirimi aşağıdaki gibidir: [public] function R name(P1 p1, … Pn pn)
Bu nedenle, function
ile ön eki olmalıdır. Ön eki public
ise, C++'dan çağrılabilir. İşlev değeri döndürmezse, dönüş türünün varsayılan değerini değerlendirecektir.
İlk ifadede bir bildirimle for
-loop'a izin veriyoruz. Ayrıca, C++17'de olduğu gibi, bir başlatma ifadesi ile if
-ifadesine ve switch
-ifadesine izin veririz. if
-ifadesi bir if
-block ile başlar, ardından sıfır veya daha fazla elif
-block ve isteğe bağlı olarak bir else
-block ile devam eder. Değişken if
- ifadesinin başlatma ifadesinde bildirilmişse, bu blokların her birinde görünür olacaktır.
Birden çok iç içe döngüden kopabilen bir break
ifadesinden sonra isteğe bağlı bir sayıya izin veriyoruz. Böylece aşağıdaki koda sahip olabilirsiniz:
for (number i = 0; i < 100; ++i) { for(number j = 0; j < 100; ++j) { if (rnd(100) == 0) { break 2; } } }
Ayrıca, her iki döngüden de kopacaktır. Bu sayı derleme zamanında doğrulanır. Ne kadar serin?
Derleyici
Bu bölüme birçok özellik eklendi, ancak çok fazla ayrıntıya girersem, muhtemelen hala benimle olan en ısrarcı okuyucuları bile kaybedeceğim. Bu nedenle, kasıtlı olarak hikayenin çok büyük bir bölümünü atlayacağım - derleme.
Çünkü bu blog serisinin birinci ve ikinci bölümlerinde zaten anlatmıştım. İfadelere odaklanıyordum, ancak başka bir şeyi derlemek çok farklı değil.
Ancak size bir örnek vereceğim. Bu kod while
deyimlerini derler:
statement_ptr compile_while_statement( compiler_context& ctx, tokens_iterator& it, possible_flow pf ) { parse_token_value(ctx, it, reserved_token::kw_while); parse_token_value(ctx, it, reserved_token::open_round); expression<number>::ptr expr = build_number_expression(ctx, it); parse_token_value(ctx, it, reserved_token::close_round); block_statement_ptr block = compile_block_statement(ctx, it, pf); return create_while_statement(std::move(expr), std::move(block)); }
Gördüğünüz gibi, karmaşık olmaktan uzak. while
, then (
, ayrıştırır, sonra bir sayı ifadesi oluşturur ( booleanlarımız yoktur) ve sonra ayrıştırır )
.
Daha sonra {
ve }
içinde olan veya olmayan bir blok deyimi derler (evet, tek deyim bloklarına izin verdim) ve sonunda bir while
deyimi oluşturur.
İlk iki fonksiyon argümanına zaten aşinasınız. Üçüncüsü, possible_flow
, ayrıştırdığımız bağlamda izin verilen akış değiştirme komutlarını ( continue
, break
, return
) gösterir. Derleme ifadeleri bazı compiler
sınıflarının üye işlevleri olsaydı, bu bilgiyi nesnede tutabilirdim, ancak mamut sınıflarının büyük bir hayranı değilim ve derleyici kesinlikle böyle bir sınıf olurdu. Fazladan bir argüman, özellikle de zayıf olandan geçmek kimseye zarar vermez ve kim bilir, belki bir gün kodu paralel hale getirebiliriz.
Derlemenin burada açıklamak istediğim bir başka ilginç yönü daha var.
İki fonksiyonun birbirini çağırdığı bir senaryoyu desteklemek istiyorsak, bunu C-yoluyla yapabiliriz: ileri bildirime izin vererek veya iki derleme aşamasına sahip olarak.
Ben ikinci yaklaşımı seçtim. İşlev tanımı bulunduğunda, türünü ve adını incomplete_function
adlı nesneye ayrıştıracağız. Ardından, ilk küme parantezini kapatana kadar küme parantezlerinin yuvalanma seviyesini sayarak, herhangi bir yorum yapmadan gövdesini atlayacağız. İşlemde belirteçleri toplayacağız, bunları incomplete_function
içinde tutacağız ve compiler_context
içine bir işlev tanımlayıcısı ekleyeceğiz.
Tüm dosyayı geçtikten sonra, çalışma zamanında çağrılabilmeleri için fonksiyonların her birini tamamen derleyeceğiz. Bu şekilde, her fonksiyon dosyadaki herhangi bir başka fonksiyonu çağırabilir ve herhangi bir global değişkene erişebilir.
Global değişkenler, aynı fonksiyonlara yapılan çağrılarla başlatılabilir, bu da bu fonksiyonlar başlatılmamış değişkenlere erişir erişmez bizi hemen eski "tavuk ve yumurta" sorununa götürür.
Böyle bir şey olursa, sorun bir runtime_exception
atılarak çözülür - ve bu sadece ben iyi olduğum için. Franky, erişim ihlali, böyle bir kod yazdığın için ceza olarak alabileceğin en az şey.
Küresel Kapsam
Global kapsamda görünebilecek iki tür varlık vardır:
- genel değişkenler
- Fonksiyonlar
Her global değişken, doğru türü döndüren bir ifadeyle başlatılabilir. Başlatıcı, her global değişken için oluşturulur.
Her başlatıcı, lvalue
döndürür, böylece bunlar global değişkenlerin yapıcıları olarak işlev görür. Genel bir değişken için herhangi bir ifade sağlanmadığında, varsayılan başlatıcı oluşturulur.
Bu, initialize
içindeki ilk üye runtime_context
:
void runtime_context::initialize() { _globals.clear(); for (const auto& initializer : _initializers) { _globals.emplace_back(initializer->evaluate(*this)); } }
Yapıcıdan çağrılır. runtime_context
durumunu sıfırlamak için açıkça çağrılabileceği için genel değişken kapsayıcısını temizler.
Daha önce bahsettiğim gibi, başlatılmamış bir global değişkene erişip erişmediğimizi kontrol etmemiz gerekiyor. Bu nedenle, bu global değişken erişimcisidir:
variable_ptr& runtime_context::global(int idx) { runtime_assertion( idx < _globals.size(), "Uninitialized global variable access" ); return _globals[idx]; }
İlk bağımsız değişken false
olarak değerlendirilirse, runtime_assertion
ilgili mesajla birlikte bir runtime_error
gönderir.
Her işlev, daha sonra işlevin aldığı runtime_context
ile değerlendirilen tek ifadeyi yakalayan lambda olarak uygulanır.
İşlev Kapsamı
while
-ifade derlemesinden de görebileceğiniz gibi, derleyici, tüm fonksiyonun bloğunu temsil eden blok ifadesinden başlayarak özyinelemeli olarak çağrılır.
İşte tüm ifadeler için soyut temel sınıf:
class statement { statement(const statement&) = delete; void operator=(const statement&) = delete; protected: statement() = default; public: virtual flow execute(runtime_context& context) = 0; virtual ~statement() = default; };
Varsayılan işlevler dışındaki tek işlev, deyim mantığını execute
üzerinde gerçekleştiren ve program mantığının bir sonraki nereye gideceğini belirleyen flow
döndüren runtime_context
.
enum struct flow_type{ f_normal, f_break, f_continue, f_return, }; class flow { private: flow_type _type; int _break_level; flow(flow_type type, int break_level); public: flow_type type() const; int break_level() const; static flow normal_flow(); static flow break_flow(int break_level); static flow continue_flow(); static flow return_flow(); flow consume_break(); };
Statik oluşturucu işlevleri kendi kendini açıklayıcıdır ve bunları sıfır olmayan break_level
ve flow_type::f_break
türünden farklı türde mantıksız flow
önlemek için yazdım.
Şimdi, consume_break
, bir kesinti seviyesi daha az olan bir kesinti akışı veya kesinti seviyesi sıfıra ulaşırsa normal akış oluşturur.
Şimdi tüm ifade türlerini kontrol edeceğiz:
class simple_statement: public statement { private: expression<void>::ptr _expr; public: simple_statement(expression<void>::ptr expr): _expr(std::move(expr)) { } flow execute(runtime_context& context) override { _expr->evaluate(context); return flow::normal_flow(); } };
Burada simple_statement
, bir ifadeden oluşturulan ifadedir. Her ifade void
döndüren bir ifade olarak derlenebilir, böylece ondan simple_statement
oluşturulabilir. Ne break
, ne continue
etme ne de return
bir ifadenin parçası olamayacağından, simple_statement
, flow::normal_flow()
döndürür.
class block_statement: public statement { private: std::vector<statement_ptr> _statements; public: block_statement(std::vector<statement_ptr> statements): _statements(std::move(statements)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const statement_ptr& statement : _statements) { if ( flow f = statement->execute(context); f.type() != flow_type::f_normal ){ return f; } } return flow::normal_flow(); } };
block_statement
, ifadelerin std::vector
vector'unu tutar. Bunları birer birer yürütür. Her biri normal olmayan akışı döndürürse, hemen o akışı döndürür. Yerel kapsam değişkeni bildirimlerine izin vermek için bir RAII kapsam nesnesi kullanır.
class local_declaration_statement: public statement { private: std::vector<expression<lvalue>::ptr> _decls; public: local_declaration_statement(std::vector<expression<lvalue>::ptr> decls): _decls(std::move(decls)) { } flow execute(runtime_context& context) override { for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return flow::normal_flow(); } };
local_declaration_statement
, yerel bir değişken oluşturan ifadeyi değerlendirir ve yeni yerel değişkeni yığına iter.
class break_statement: public statement { private: int _break_level; public: break_statement(int break_level): _break_level(break_level) { } flow execute(runtime_context&) override { return flow::break_flow(_break_level); } };
break_statement
, derleme zamanında değerlendirilen kesme düzeyine sahiptir. Sadece o kırılma düzeyine karşılık gelen akışı döndürür.
class continue_statement: public statement { public: continue_statement() = default; flow execute(runtime_context&) override { return flow::continue_flow(); } };
continue_statement
sadece flow::continue_flow()
döndürür.
class return_statement: public statement { private: expression<lvalue>::ptr _expr; public: return_statement(expression<lvalue>::ptr expr) : _expr(std::move(expr)) { } flow execute(runtime_context& context) override { context.retval() = _expr->evaluate(context); return flow::return_flow(); } }; class return_void_statement: public statement { public: return_void_statement() = default; flow execute(runtime_context&) override { return flow::return_flow(); } };
return_statement
ve return_void_statement
her ikisi de return flow::return_flow()
. Tek fark, birincisinin, dönmeden önce dönüş değeri olarak değerlendirdiği ifadeye sahip olmasıdır.

class if_statement: public statement { private: std::vector<expression<number>::ptr> _exprs; std::vector<statement_ptr> _statements; public: if_statement( std::vector<expression<number>::ptr> exprs, std::vector<statement_ptr> statements ): _exprs(std::move(exprs)), _statements(std::move(statements)) { } flow execute(runtime_context& context) override { for (size_t i = 0; i < _exprs.size(); ++i) { if (_exprs[i]->evaluate(context)) { return _statements[i]->execute(context); } } return _statements.back()->execute(context); } }; class if_declare_statement: public if_statement { private: std::vector<expression<lvalue>::ptr> _decls; public: if_declare_statement( std::vector<expression<lvalue>::ptr> decls, std::vector<expression<number>::ptr> exprs, std::vector<statement_ptr> statements ): if_statement(std::move(exprs), std::move(statements)), _decls(std::move(decls)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return if_statement::execute(context); } };
bir if
if_statement
, sıfır veya daha fazla elif
-block ve bir else
-block (boş olabilir) için oluşturulan if_statement, bir ifade 1
olarak değerlendirilene kadar her ifadesini değerlendirir. Daha sonra bu bloğu yürütür ve yürütme sonucunu döndürür. Hiçbir ifade 1
olarak değerlendirilmezse, son ( else
) bloğun yürütülmesini döndürür.
if_declare_statement
, if-cümlesinin ilk parçası olarak bildirimleri olan ifadedir. Bildirilen tüm değişkenleri yığına iter ve ardından temel sınıfını ( if_statement
) yürütür.
class switch_statement: public statement { private: expression<number>::ptr _expr; std::vector<statement_ptr> _statements; std::unordered_map<number, size_t> _cases; size_t _dflt; public: switch_statement( expression<number>::ptr expr, std::vector<statement_ptr> statements, std::unordered_map<number, size_t> cases, size_t dflt ): _expr(std::move(expr)), _statements(std::move(statements)), _cases(std::move(cases)), _dflt(dflt) { } flow execute(runtime_context& context) override { auto it = _cases.find(_expr->evaluate(context)); for ( size_t idx = (it == _cases.end() ? _dflt : it->second); idx < _statements.size(); ++idx ) { switch (flow f = _statements[idx]->execute(context); f.type()) { case flow_type::f_normal: break; case flow_type::f_break: return f.consume_break(); default: return f; } } return flow::normal_flow(); } }; class switch_declare_statement: public switch_statement { private: std::vector<expression<lvalue>::ptr> _decls; public: switch_declare_statement( std::vector<expression<lvalue>::ptr> decls, expression<number>::ptr expr, std::vector<statement_ptr> statements, std::unordered_map<number, size_t> cases, size_t dflt ): _decls(std::move(decls)), switch_statement(std::move(expr), std::move(statements), std::move(cases), dflt) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return switch_statement::execute(context); } };
switch_statement
, ifadelerini birer birer yürütür, ancak önce ifade değerlendirmesinden aldığı uygun dizine atlar. İfadelerinden herhangi biri normal olmayan bir akış döndürürse, o akışı hemen döndürür. flow_type::f_break
varsa, önce bir ara tüketir.
switch_declare_statement
, başlığında bir bildirime izin verir. Bunların hiçbiri vücutta bir beyana izin vermez.
class while_statement: public statement { private: expression<number>::ptr _expr; statement_ptr _statement; public: while_statement(expression<number>::ptr expr, statement_ptr statement): _expr(std::move(expr)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { while (_expr->evaluate(context)) { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } return flow::normal_flow(); } };
class do_statement: public statement { private: expression<number>::ptr _expr; statement_ptr _statement; public: do_statement(expression<number>::ptr expr, statement_ptr statement): _expr(std::move(expr)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { do { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } while (_expr->evaluate(context)); return flow::normal_flow(); } };
while_statement
ve do_while_statement
ifadeleri 1
olarak değerlendirilirken body deyimlerini yürütür. Yürütme, flow_type::f_break
döndürürse, onu tüketir ve geri döner. flow_type::f_return
döndürürse, döndürürler. Normal yürütme veya devam etme durumunda hiçbir şey yapmazlar.
continue
etmenin bir etkisi yokmuş gibi görünebilir. Ancak, iç ifade bundan etkilendi. Örneğin, block_statement
, sonuna kadar değerlendirme yapmadı.
while_statement
C++ while
ile, do-statement
ise C++ do-while
ile uygulanmasını uygun buluyorum.
class for_statement_base: public statement { private: expression<number>::ptr _expr2; expression<void>::ptr _expr3; statement_ptr _statement; public: for_statement_base( expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): _expr2(std::move(expr2)), _expr3(std::move(expr3)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { for (; _expr2->evaluate(context); _expr3->evaluate(context)) { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } return flow::normal_flow(); } }; class for_statement: public for_statement_base { private: expression<void>::ptr _expr1; public: for_statement( expression<void>::ptr expr1, expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): for_statement_base( std::move(expr2), std::move(expr3), std::move(statement) ), _expr1(std::move(expr1)) { } flow execute(runtime_context& context) override { _expr1->evaluate(context); return for_statement_base::execute(context); } }; class for_declare_statement: public for_statement_base { private: std::vector<expression<lvalue>::ptr> _decls; expression<number>::ptr _expr2; expression<void>::ptr _expr3; statement_ptr _statement; public: for_declare_statement( std::vector<expression<lvalue>::ptr> decls, expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): for_statement_base( std::move(expr2), std::move(expr3), std::move(statement) ), _decls(std::move(decls)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return for_statement_base::execute(context); } };
for_statement
ve for_statement_declare
while_statement
ve do_statement
ile benzer şekilde uygulanır. Mantığın çoğunu yapan for_statement_base
sınıfından miras alınırlar. for_statement_declare
, for
-loop'un ilk kısmı değişken bildirimi olduğunda oluşturulur.

Bunların hepsi sahip olduğumuz deyim sınıflarıdır. Onlar bizim işlevlerimizin yapı taşlarıdır. runtime_context
oluşturulduğunda, bu işlevleri tutar. İşlev public
anahtar sözcüğüyle bildirilmişse, adıyla çağrılabilir.
Bu, Stork'un temel işlevselliğini tamamlar. Anlatacağım diğer her şey, dilimizi daha kullanışlı hale getirmek için eklediğim düşüncelerdir.
demetler
Diziler, yalnızca tek bir türden öğeler içerebildikleri için homojen kaplardır. Heterojen kaplar istersek hemen akla yapılar gelir.
Bununla birlikte, daha önemsiz heterojen kaplar vardır: demetler. Tuple'lar farklı türdeki öğeleri tutabilir, ancak türlerinin derleme zamanında bilinmesi gerekir. Bu, Stork'ta bir demet bildirimi örneğidir:
[number, string] t = {22321, "Siveric"};
Bu, number
ve string
çiftini bildirir ve onu başlatır.
Başlatma listeleri, dizileri başlatmak için de kullanılabilir. Başlatma listesindeki ifade türleri değişken türüyle eşleşmediğinde bir derleyici hatası oluşur.
Diziler variable_ptr
kapsayıcıları olarak uygulandığından, demetlerin çalışma zamanı uygulamasını ücretsiz olarak aldık. İçerilen değişkenlerin doğru türünü temin ettiğimizde derleme zamanıdır.
Modüller
Uygulama detaylarını bir Stork kullanıcısından gizlemek ve dili daha kullanıcı dostu bir şekilde sunmak güzel olurdu.
Bunu başarmamıza yardımcı olacak sınıf budur. Uygulama detayları olmadan sunuyorum:
class module { ... public: template<typename R, typename... Args> void add_external_function(const char* name, std::function<R(Args...)> f); template<typename R, typename... Args> auto create_public_function_caller(std::string name); void load(const char* path); bool try_load(const char* path, std::ostream* err = nullptr) noexcept; void reset_globals(); ... };
load
ve try_load
işlevleri, verilen yoldan Stork betiğini yükleyecek ve derleyecektir. Birincisi, bunlardan biri stork::error
fırlatabilir, ancak ikincisi onu yakalar ve varsa çıktıya yazdırır.
reset_globals
işlevi, global değişkenleri yeniden başlatır.
add_external_functions
ve create_public_function_caller
işlevleri derlemeden önce çağrılmalıdır. İlki, Stork'tan çağrılabilecek bir C++ işlevi ekler. İkincisi, C++'dan Stork işlevini çağırmak için kullanılabilecek çağrılabilir nesneyi yaratır. Stork komut dosyası derlemesi sırasında genel işlev türü R(Args…)
ile eşleşmezse derleme zamanı hatasına neden olur.
Stork modülüne eklenebilecek birkaç standart fonksiyon ekledim.
void add_math_functions(module& m); void add_string_functions(module& m); void add_trace_functions(module& m); void add_standard_functions(module& m);
Örnek vermek
İşte bir Stork komut dosyası örneği:
function void swap(number& x, number& y) { number tmp = x; x = y; y = tmp; } function void quicksort( number[]& arr, number begin, number end, number(number, number) comp ) { if (end - begin < 2) return; number pivot = arr[end-1]; number i = begin; for (number j = begin; j < end-1; ++j) if (comp(arr[j], pivot)) swap(&arr[i++], &arr[j]); swap (&arr[i], &arr[end-1]); quicksort(&arr, begin, i, comp); quicksort(&arr, i+1, end, comp); } function void sort(number[]& arr, number(number, number) comp) { quicksort(&arr, 0, sizeof(arr), comp); } function number less(number x, number y) { return x < y; } public function void main() { number[] arr; for (number i = 0; i < 100; ++i) { arr[sizeof(arr)] = rnd(100); } trace(tostring(arr)); sort(&arr, less); trace(tostring(arr)); sort(&arr, greater); trace(tostring(arr)); }
İşte C++ kısmı:
#include <iostream> #include "module.hpp" #include "standard_functions.hpp" int main() { std::string path = __FILE__; path = path.substr(0, path.find_last_of("/\\") + 1) + "test.stk"; using namespace stork; module m; add_standard_functions(m); m.add_external_function( "greater", std::function<number(number, number)>([](number x, number y){ return x > y; } )); auto s_main = m.create_public_function_caller<void>("main"); if (m.try_load(path.c_str(), &std::cerr)) { s_main(); } return 0; }
Modüle derlemeden önce standart işlevler eklenir ve Stork betiğinden trace
ve rnd
işlevleri kullanılır. Daha greater
işlevi de bir vitrin olarak eklenir.
Komut dosyası, “main.cpp” ile aynı klasörde bulunan “test.stk” dosyasından yüklenir (bir __FILE__
önişlemci tanımı kullanılarak) ve ardından main
işlevi çağrılır.
Komut dosyasında, less
karşılaştırıcıyı kullanarak artan şekilde ve ardından C++ ile yazılmış greater
karşılaştırıcıyı kullanarak azalan düzende sıralayarak rastgele bir dizi oluştururuz.
Kodun, C'de (veya C'den türetilen herhangi bir programlama dilinde) akıcı olan herkes için mükemmel bir şekilde okunabilir olduğunu görebilirsiniz.
Sonra ne yapacağız?
Stork'ta uygulamak istediğim birçok özellik var:
- Yapılar
- Sınıflar ve miras
- Modüller arası çağrılar
- Lambda fonksiyonları
- Dinamik olarak yazılan nesneler
Zaman ve mekan eksikliği, bunları halihazırda uygulamamamızın nedenlerinden biridir. Boş zamanlarımda yeni özellikler uygularken GitHub sayfamı yeni sürümlerle güncellemeye çalışacağım.
Toplama
Yeni bir programlama dili yarattık!
Bu, son altı haftadaki boş zamanımın büyük bir bölümünü aldı, ancak şimdi bazı senaryolar yazıp bunların çalıştığını görebilirim. Son birkaç gündür yaptığım şey, beklenmedik bir şekilde her çarptığında kel kafamı kaşımaktı. Bazen küçük bir böcek, bazen de kötü bir böcekti. Ancak diğer zamanlarda, dünyayla zaten paylaştığım kötü bir kararla ilgili olduğu için utandım. Ama her seferinde düzeltip kodlamaya devam ederdim.
Bu süreçte daha önce hiç kullanmadığım if constexpr
öğrendim. C++17'nin her gün karşılaşmadığım diğer küçük özelliklerinin yanı sıra, değer referansları ve mükemmel yönlendirme ile daha aşina oldum.
Kod mükemmel değil - asla böyle bir iddiada bulunmam - ama yeterince iyi ve çoğunlukla iyi programlama uygulamalarını takip ediyor. Ve en önemlisi - işe yarıyor.
Sıfırdan yeni bir dil geliştirmeye karar vermek, ortalama bir insana, hatta ortalama bir programcıya çılgınca gelebilir, ancak bu, bunu yapmak ve yapabileceğinizi kendinize kanıtlamak için daha fazla nedendir. Tıpkı zor bir bulmacayı çözmenin zihinsel olarak formda kalmak için iyi bir beyin egzersizi olması gibi.
Sadece ilginç yönlerini seçemeyeceğimiz ve bazen sıkıcı olsa bile ciddi işler yapmamız gerektiği için günlük programlamamızda sıkıcı zorluklar yaygındır. Profesyonel bir geliştiriciyseniz, ilk önceliğiniz işvereninize yüksek kaliteli kod teslim etmek ve masaya yemek koymaktır. Bu bazen boş zamanlarınızda programlama yapmaktan kaçınmanıza neden olabilir ve erken programlama okul günlerinizin coşkusunu azaltabilir.
Mecbur değilseniz, bu hevesinizi kaybetmeyin. Zaten yapılmış olsa bile, ilginç bulursanız bir şey üzerinde çalışın. Biraz eğlenmek için sebep göstermenize gerek yok.
Ve eğer bunu kısmen de olsa profesyonel işinize dahil edebiliyorsanız, ne mutlu size! Pek çok insan bu fırsata sahip değil.
Bu bölümün kodu GitHub sayfamda özel bir dal ile dondurulacak.