Stork, Partea 4: Implementarea declarațiilor și încheierea
Publicat: 2022-03-11În încercarea noastră de a crea un limbaj de programare ușor folosind C++, am început prin a crea tokenizatorul nostru acum trei săptămâni, iar apoi am implementat evaluarea expresiei în următoarele două săptămâni.
Acum, este timpul să încheiem și să livrăm un limbaj de programare complet care nu va fi la fel de puternic ca un limbaj de programare matur, dar va avea toate caracteristicile necesare, inclusiv o amprentă foarte mică.
Mi se pare amuzant cum noile companii au secțiuni de întrebări frecvente pe site-urile lor web care nu răspund la întrebările care sunt adresate frecvent, ci la întrebări la care doresc să le fie adresate. La fel voi face aici. Oamenii care îmi urmăresc munca mă întreabă adesea de ce Stork nu se compilează într-un bytecode sau cel puțin într-un limbaj intermediar.
De ce nu se compilează Stork în Bytecode?
Sunt bucuros să răspund la această întrebare. Scopul meu a fost să dezvolt un limbaj de scripting cu amprentă mică, care să se integreze cu ușurință cu C++. Nu am o definiție strictă pentru „amprentă mică”, dar îmi imaginez un compilator care va fi suficient de mic pentru a permite portabilitatea pe dispozitive mai puțin puternice și nu va consuma prea multă memorie atunci când este rulat.
Nu m-am concentrat pe viteză, deoarece cred că veți codifica în C++ dacă aveți o sarcină critică în timp, dar dacă aveți nevoie de un fel de extensibilitate, atunci un limbaj precum Stork ar putea fi util.
Nu susțin că nu există alte limbi mai bune care să poată îndeplini o sarcină similară (de exemplu, Lua). Ar fi cu adevărat tragic dacă nu ar exista și eu doar vă dau o idee despre cazul de utilizare al acestui limbaj.
Deoarece va fi încorporat în C++, mi se pare util să folosesc unele caracteristici existente ale C++ în loc să scrie un întreg ecosistem care va servi unui scop similar. Nu numai atât, dar și această abordare mi se pare mai interesantă.
Ca întotdeauna, puteți găsi codul sursă complet pe pagina mea GitHub. Acum, să aruncăm o privire mai atentă la progresul nostru.
Schimbări
Până în această parte, Stork a fost un produs parțial complet, așa că nu am putut să-i văd toate dezavantajele și defectele. Cu toate acestea, deoarece a luat o formă mai completă, am schimbat următoarele lucruri introduse în părțile anterioare:
- Funcțiile nu mai sunt variabile. Există un
function_lookup
separat încompiler_context
acum.function_param_lookup
este redenumit pentruparam_lookup
pentru a evita confuzia. - Am schimbat modul în care sunt numite funcțiile. Există metoda de
call
înruntime_context
care preiastd::vector
de argumente, stochează vechiul index al valorii returnate, împinge argumentele în stivă, modifică indexul valorii returnate, apelează funcția, scoate argumente din stivă, restabilește vechiul index al valorii returnate și returnează rezultatul. În acest fel, nu trebuie să păstrăm stiva de indici de valoare de returnare, ca înainte, deoarece stiva C++ servește acestui scop. - Clasele RAII adăugate în
compiler_context
care sunt returnate prin apeluri lascope
șifunction
funcțiilor sale membre. Fiecare dintre aceste obiecte creează noilocal_identifier_lookup
și, respectiv,param_identifier_lookup
în constructorii lor și restabilește starea veche în destructor. - O clasă RAII adăugată în
runtime_context
, returnată de funcția membruget_scope
. Această funcție stochează dimensiunea stivei în constructorul său și o restabilește în destructorul său. - Am eliminat cuvântul cheie
const
și obiectele constante în general. Ele pot fi utile, dar nu sunt absolut necesare. - cuvântul cheie
var
a fost eliminat, deoarece în prezent nu este deloc necesar. - Am adăugat cuvântul cheie
sizeof
, care va verifica dimensiunea unei matrice în timpul de execuție. Poate că unii programatori C++ vor considera că alegerea numelui este blasfemiană, deoarece C++sizeof
rulează în timpul compilării, dar am ales acel cuvânt cheie pentru a evita coliziunea cu un nume de variabilă comun - de exemplu,size
. - Am adăugat cuvântul cheie
tostring
, care convertește în mod explicit orice înstring
. Nu poate fi o funcție, deoarece nu permitem supraîncărcarea funcției. - Diverse modificări mai puțin interesante.
Sintaxă
Deoarece folosim o sintaxă foarte asemănătoare cu C și limbajele sale de programare aferente, vă voi oferi doar detalii care ar putea să nu fie clare.
Declarațiile de tip variabilă sunt după cum urmează:
-
void
, folosit numai pentru tipul de returnare a funcției -
number
-
string
-
T[]
este o matrice a ceea ce conține elemente de tipT
-
R(P1,...,Pn)
este o funcție care returnează tipulR
și primește argumente de tipurileP1
laPn
. Fiecare dintre aceste tipuri poate fi prefixat cu&
dacă este transmis prin referință.
Declarația funcției este după cum urmează: [public] function R name(P1 p1, … Pn pn)
Deci, trebuie să fie prefixat cu function
. Dacă este prefixat cu public
, atunci poate fi apelat din C++. Dacă funcția nu returnează valoarea, va evalua valoarea implicită a tipului său returnat.
Permitem -loop for
o declarație în prima expresie. De asemenea, permitem if
-statement și switch
-statement cu o expresie de inițializare, ca în C++17. Declarația if
începe cu un bloc if
, urmat de zero sau mai multe blocuri elif
și, opțional, un else
bloc. Dacă variabila a fost declarată în expresia de inițializare a instrucțiunii if
, ar fi vizibilă în fiecare dintre acele blocuri.
Permitem un număr opțional după o instrucțiune break
care se poate rupe din mai multe bucle imbricate. Deci puteți avea următorul cod:
for (number i = 0; i < 100; ++i) { for(number j = 0; j < 100; ++j) { if (rnd(100) == 0) { break 2; } } }
De asemenea, se va rupe din ambele bucle. Acest număr este validat în timpul compilării. Cat de tare e asta?
Compilator
Multe caracteristici au fost adăugate în această parte, dar dacă devin prea detaliat, probabil că voi pierde chiar și pe cei mai persistenti cititori care încă mă suportă. Prin urmare, voi sări peste o parte foarte mare a poveștii - compilația.
Asta pentru că am descris-o deja în prima și a doua parte a acestei serii de bloguri. M-am concentrat pe expresii, dar a compila orice altceva nu este mult diferit.
Îți voi da, totuși, un exemplu. Acest cod compilează declarațiile while
:
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)); }
După cum puteți vedea, este departe de a fi complicat. Parsează while
, apoi (
, apoi construiește o expresie numerică (nu avem booleani) și apoi parsează )
.
După aceea, compilează o instrucțiune de bloc care poate fi în interiorul {
și }
sau nu (da, am permis blocuri cu o singură instrucțiune) și creează o instrucțiune while
la sfârșit.
Sunteți deja familiarizat cu primele două argumente ale funcției. Al treilea, possible_flow
, arată comenzile de schimbare a fluxului permise ( continue
, break
, return
) în contextul pe care îl analizăm. Aș putea păstra acele informații în obiect dacă instrucțiunile de compilare ar fi funcții membre ale unei clase de compiler
, dar nu sunt un mare fan al claselor mamut, iar compilatorul ar fi cu siguranță o astfel de clasă. Trecerea unui argument în plus, mai ales subțire, nu va răni nimănui și cine știe, poate într-o zi vom reuși să paralelizăm codul.
Mai este un aspect interesant al compilației pe care aș dori să-l explic aici.
Dacă vrem să susținem un scenariu în care două funcții se apelează reciproc, putem face acest lucru pe calea C: permițând declararea înainte sau având două faze de compilare.
Am ales a doua abordare. Când definiția funcției este găsită, vom analiza tipul și numele acesteia în obiectul numit incomplete_function
. Apoi, îi vom sări peste corpul, fără o interpretare, pur și simplu numărând nivelul de imbricare al bretelelor până când închidem prima bretele. Vom colecta token-uri în acest proces, le vom păstra în incomplete_function
și vom adăuga un identificator de funcție în compiler_context
.
Odată ce trecem întregul fișier, vom compila fiecare dintre funcții complet, astfel încât acestea să poată fi apelate în timpul de execuție. În acest fel, fiecare funcție poate apela orice altă funcție din fișier și poate accesa orice variabilă globală.
Variabilele globale pot fi inițializate prin apeluri la aceleași funcții, ceea ce ne duce imediat la vechea problemă „găină și ou” de îndată ce acele funcții accesează variabile neinițializate.
Dacă se întâmplă vreodată, problema este rezolvată prin lansarea unei runtime_exception
asta doar pentru că sunt drăguț. Franky, încălcarea accesului este cel mai puțin pe care îl poți obține ca pedeapsă pentru scrierea unui astfel de cod.
Domeniul de aplicare global
Există două tipuri de entități care pot apărea în domeniul global:
- Variabile globale
- Funcții
Fiecare variabilă globală poate fi inițializată cu o expresie care returnează tipul corect. Inițializatorul este creat pentru fiecare variabilă globală.
Fiecare inițializator returnează lvalue
, deci servesc ca constructori ai variabilelor globale. Când nu este furnizată nicio expresie pentru o variabilă globală, inițializatorul implicit este construit.
Aceasta este funcția membru de initialize
în runtime_context
:
void runtime_context::initialize() { _globals.clear(); for (const auto& initializer : _initializers) { _globals.emplace_back(initializer->evaluate(*this)); } }
Este apelat de la constructor. Șterge containerul de variabile globale, așa cum poate fi numit în mod explicit, pentru a reseta starea runtime_context
.
După cum am menționat mai devreme, trebuie să verificăm dacă accesăm o variabilă globală neinițializată. Prin urmare, acesta este accesorul variabilei globale:
variable_ptr& runtime_context::global(int idx) { runtime_assertion( idx < _globals.size(), "Uninitialized global variable access" ); return _globals[idx]; }
Dacă primul argument este false
, runtime_assertion
aruncă un runtime_error
cu mesajul corespunzător.
Fiecare funcție este implementată ca lambda care captează o singură instrucțiune, care este apoi evaluată cu runtime_context
pe care o primește funcția.
Domeniul de aplicare
După cum ați putut vedea din compilarea instrucțiunii while
, compilatorul este apelat recursiv, începând cu instrucțiunea bloc, care reprezintă blocul întregii funcție.
Iată clasa de bază abstractă pentru toate declarațiile:
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; };
Singura funcție în afară de cele implicite este execute
, care realizează logica instrucțiunii pe runtime_context
și returnează flow
, care determină unde va merge logica programului.
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(); };
Funcțiile creatoare statice se explică de la sine și le-am scris pentru a preveni flow
ilogic cu break_level
diferit de zero și tip diferit de flow_type::f_break
.
Acum, consume_break
va crea un flux de pauză cu un nivel de pauză mai puțin sau, dacă nivelul de pauză ajunge la zero, fluxul normal.
Acum, vom verifica toate tipurile de declarații:
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(); } };
Aici, simple_statement
este declarația care este creată dintr-o expresie. Fiecare expresie poate fi compilată ca o expresie care returnează void
, astfel încât simple_statement
poate fi creată din ea. Deoarece nici break
, continue
sau return
nu poate face parte dintr-o expresie, simple_statement
returnează flow::normal_flow()
.
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
păstrează std::vector
instrucțiunilor. Le execută, unul câte unul. Dacă fiecare dintre ele returnează un flux nenormal, acesta returnează acel flux imediat. Folosește un obiect RAII pentru a permite declararea variabilelor locale.
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
evaluează expresia care creează o variabilă locală și împinge noua variabilă locală în stivă.
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
are nivelul de pauză evaluat în timpul compilării. Acesta returnează doar fluxul care corespunde acelui nivel de pauză.
class continue_statement: public statement { public: continue_statement() = default; flow execute(runtime_context&) override { return flow::continue_flow(); } };
continue_statement
returnează doar flow::continue_flow()
.
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
și return_void_statement
ambele returnează flow::return_flow()
. Singura diferență este că primul are expresia pe care o evaluează la valoarea returnată înainte de a reveni.

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); } };
if_statement
, care este creat pentru un if
-block, zero sau mai multe elif
-blocks și un else
-block (care ar putea fi gol), evaluează fiecare dintre expresiile sale până când o expresie evaluează la 1
. Apoi execută acel bloc și returnează rezultatul execuției. Dacă nicio expresie nu evaluează la 1
, va returna execuția ultimului bloc ( else
).
if_declare_statement
este instrucțiunea care are declarații ca prima parte a unei clauze if. Împinge toate variabilele declarate în stivă și apoi își execută clasa de bază ( if_statement
).
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
își execută instrucțiunile unul câte unul, dar mai întâi sare la indexul corespunzător pe care îl obține din evaluarea expresiei. Dacă oricare dintre instrucțiunile sale returnează un flux nenormal, va returna acel flux imediat. Dacă are flow_type::f_break
, va consuma mai întâi o pauză.
switch_declare_statement
permite o declarație în antetul său. Niciuna dintre acestea nu permite o declarație în organism.
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
și do_while_statement
își execută ambele instrucțiuni body în timp ce expresia lor evaluează la 1
. Dacă execuția returnează flow_type::f_break
, ei îl consumă și revin. Dacă returnează flow_type::f_return
, îl returnează. În cazul execuției normale, sau continuă, nu fac nimic.
Poate părea că continue
nu are efect. Cu toate acestea, declarația interioară a fost afectată de aceasta. Dacă a fost, de exemplu, block_statement
, nu a fost evaluat până la sfârșit.
Mi se pare bine că while_statement
este implementat cu C++ while
, iar do-statement
cu C++ do-while
.
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
și for_statement_declare
sunt implementate în mod similar ca while_statement
și do_statement
. Ele sunt moștenite din clasa for_statement_base
, care face cea mai mare parte a logicii. for_statement_declare
este creat atunci când prima parte a buclei for
este o declarație variabilă.

Acestea sunt toate clasele de declarații pe care le avem. Ele sunt elemente de bază ale funcțiilor noastre. Când runtime_context
este creat, păstrează acele funcții. Dacă funcția este declarată cu cuvântul cheie public
, aceasta poate fi apelată după nume.
Aceasta încheie funcționalitatea de bază a Stork. Toate celelalte pe care le voi descrie sunt gânduri ulterioare pe care le-am adăugat pentru a face limba noastră mai utilă.
Tupluri
Matricele sunt containere omogene, deoarece pot conține elemente de un singur tip. Dacă vrem containere eterogene, imediat ne vin în minte structurile.
Cu toate acestea, există containere eterogene mai banale: tupluri. Tuplurile pot păstra elemente de diferite tipuri, dar tipurile lor trebuie să fie cunoscute în timpul compilării. Acesta este un exemplu de declarație de tuplu în Stork:
[number, string] t = {22321, "Siveric"};
Aceasta declară perechea de number
și string
și o inițializează.
Listele de inițializare pot fi folosite și pentru a inițializa matrice. Când tipurile de expresii din lista de inițializare nu se potrivesc cu tipul de variabilă, va apărea o eroare de compilator.
Deoarece matricele sunt implementate ca containere ale variable_ptr
, am primit gratuit implementarea tuplurilor în timp de execuție. Este timpul de compilare când asigurăm tipul corect de variabile conținute.
Module
Ar fi bine să ascundeți detaliile de implementare de la un utilizator Stork și să prezentați limba într-un mod mai ușor de utilizat.
Aceasta este clasa care ne va ajuta să realizăm asta. Îl prezint fără detaliile de implementare:
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(); ... };
Funcțiile load
și try_load
vor încărca și compila scriptul Stork din calea dată. În primul rând, unul dintre ei poate arunca o stork::error
, dar al doilea îl va prinde și îl va imprima pe ieșire, dacă este prevăzut.
Funcția reset_globals
va reinițializa variabilele globale.
Funcțiile add_external_functions
și create_public_function_caller
ar trebui apelate înainte de compilare. Primul adaugă o funcție C++ care poate fi apelată din Stork. Al doilea creează obiectul apelabil care poate fi folosit pentru a apela funcția Stork din C++. Va provoca o eroare la timp de compilare dacă tipul funcției publice nu se potrivește cu R(Args…)
în timpul compilării scriptului Stork.
Am adăugat câteva funcții standard care pot fi adăugate la modulul Stork.
void add_math_functions(module& m); void add_string_functions(module& m); void add_trace_functions(module& m); void add_standard_functions(module& m);
Exemplu
Iată un exemplu de scenariu Stork:
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)); }
Iată partea C++:
#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; }
Funcțiile standard sunt adăugate la modul înainte de compilare, iar funcțiile trace
și rnd
sunt utilizate din scriptul Stork. Funcția greater
este adăugată și ca o vitrină.
Scriptul este încărcat din fișierul „test.stk”, care se află în același folder cu „main.cpp” (prin utilizarea unei definiții de preprocesor __FILE__
), apoi este apelată funcția main
.
În script, generăm o matrice aleatorie, sortând crescător folosind comparatorul less
și apoi descrescător folosind comparatorul greater
, scris în C++.
Puteți vedea că codul este perfect lizibil pentru oricine cunoaște fluent C (sau orice limbaj de programare derivat din C).
Ce sa fac in continuare?
Există multe caracteristici pe care aș dori să le implementez în Stork:
- Structuri
- Clasele și moștenirea
- Apeluri între module
- Funcții lambda
- Obiecte tipizate dinamic
Lipsa de timp și spațiu este unul dintre motivele pentru care nu le avem deja implementate. Voi încerca să-mi actualizez pagina GitHub cu versiuni noi pe măsură ce implementez noi funcții în timpul meu liber.
Încheierea
Am creat un nou limbaj de programare!
Asta a luat o bună parte din timpul meu liber în ultimele șase săptămâni, dar acum pot să scriu câteva scripturi și să le văd rulând. Este ceea ce făceam în ultimele zile, scărpinându-mă pe chel de fiecare dată când se prăbușește pe neașteptate. Uneori, era un mic bug, iar uneori un bug urât. Alteori, însă, mă simțeam jenat pentru că era vorba despre o decizie proastă pe care deja o împărtășisem lumii. Dar de fiecare dată, reparam și continuam să codific.
În acest proces, am aflat despre if constexpr
, pe care nu l-am mai folosit niciodată. De asemenea, m-am familiarizat mai mult cu referințele rvalue și cu redirecționarea perfectă, precum și cu alte caracteristici mai mici ale C++17 pe care nu le întâlnesc zilnic.
Codul nu este perfect – nu aș face niciodată o astfel de afirmație – dar este suficient de bun și urmează în mare parte bunele practici de programare. Și cel mai important - funcționează.
Decizia de a dezvolta un nou limbaj de la zero poate suna nebunesc pentru o persoană obișnuită, sau chiar pentru un programator obișnuit, dar este cu atât mai mult motiv pentru a o face și pentru a-ți dovedi ție că poți face asta. La fel cum rezolvarea unui puzzle dificil este un bun exercițiu pentru creier pentru a rămâne în formă mentală.
Provocările plictisitoare sunt frecvente în programarea noastră de zi cu zi, deoarece nu putem alege doar aspectele interesante ale acesteia și trebuie să facem o muncă serioasă, chiar dacă uneori este plictisitor. Dacă sunteți un dezvoltator profesionist, prima dvs. prioritate este să furnizați un cod de înaltă calitate angajatorului și să puneți mâncare pe masă. Acest lucru vă poate face uneori să evitați programarea în timpul liber și vă poate atenua entuziasmul din primele zile de școală de programare.
Dacă nu trebuie, nu-ți pierde acel entuziasm. Lucrați la ceva dacă vi se pare interesant, chiar dacă este deja făcut. Nu trebuie să justificați motivul pentru a vă distra puțin.
Și dacă o poți încorpora, chiar și parțial, în munca ta profesională, bine pentru tine! Nu mulți oameni au această oportunitate.
Codul pentru această parte va fi înghețat cu o ramură dedicată pe pagina mea GitHub.