Stork, 4부: 명령문 구현 및 마무리
게시 됨: 2022-03-11C++를 사용하여 가벼운 프로그래밍 언어를 만들기 위한 탐구에서 우리는 3주 전에 토크나이저를 만드는 것으로 시작했고 그 다음 2주 동안 표현식 평가를 구현했습니다.
이제 완성된 프로그래밍 언어만큼 강력하지는 않지만 매우 작은 설치 공간을 포함하여 필요한 모든 기능을 갖춘 완전한 프로그래밍 언어를 마무리하고 제공할 때입니다.
나는 새로운 회사가 자주 묻는 질문이 아니라 그들이 묻고 싶은 질문에 대답하는 FAQ 섹션이 웹사이트에 있다는 것이 재미있다는 것을 알게 되었습니다. 여기서도 똑같이 하겠습니다. 내 작업을 따르는 사람들은 종종 Stork가 일부 바이트 코드 또는 최소한 일부 중간 언어로 컴파일되지 않는 이유를 묻습니다.
황새가 바이트 코드로 컴파일되지 않는 이유는 무엇입니까?
이 질문에 답하게 되어 기쁩니다. 제 목표는 C++와 쉽게 통합할 수 있는 소규모 스크립팅 언어를 개발하는 것이었습니다. "작은 공간"에 대한 엄격한 정의는 없지만 덜 강력한 장치에 이식할 수 있을 만큼 충분히 작고 실행할 때 너무 많은 메모리를 소비하지 않는 컴파일러를 상상합니다.
저는 속도에 중점을 두지 않았습니다. 시간이 중요한 작업이 있는 경우 C++로 코딩할 것이라고 생각하지만 일종의 확장성이 필요한 경우 Stork와 같은 언어가 유용할 수 있습니다.
비슷한 작업을 수행할 수 있는 더 나은 다른 언어가 없다고 주장하지 않습니다(예: Lua). 그것들이 존재하지 않는다면 정말 비극적일 것입니다. 저는 단지 이 언어의 사용 사례에 대한 아이디어를 제공할 뿐입니다.
C++에 내장될 것이기 때문에 비슷한 목적을 수행하는 전체 생태계를 작성하는 대신 C++의 일부 기존 기능을 사용하는 것이 편리합니다. 뿐만 아니라 이 접근 방식이 더 흥미롭습니다.
항상 그렇듯이 내 GitHub 페이지에서 전체 소스 코드를 찾을 수 있습니다. 이제 진행 상황을 자세히 살펴보겠습니다.
변경 사항
여기까지는 Stork가 부분적으로 완성된 제품이었기 때문에 단점과 단점을 모두 볼 수는 없었습니다. 그러나 더 완전한 형태를 취하면서 이전 부분에서 소개한 다음 사항을 변경했습니다.
- 함수는 더 이상 변수가 아닙니다. 이제
compiler_context
에 별도의function_lookup
이 있습니다.function_param_lookup
은 혼동을 피하기 위해param_lookup
으로 이름이 변경되었습니다. - 함수가 호출되는 방식을 변경했습니다.
runtime_context
에는 인수의std::vector
를 취하고, 이전 반환 값 인덱스를 저장하고, 스택에 인수를 푸시하고, 반환 값 인덱스를 변경하고, 함수를call
하고, 스택에서 인수를 꺼내고, 이전 반환 값 인덱스를 복원하고, 결과를 반환합니다. 그렇게 하면 C++ 스택이 그 목적을 수행하기 때문에 이전처럼 반환 값 인덱스 스택을 유지할 필요가 없습니다. - RAII 클래스는 해당 멤버 함수
scope
및function
에 대한 호출에 의해 반환되는compiler_context
에 추가되었습니다. 이러한 각 객체는 생성자에서 각각 새로운local_identifier_lookup
및param_identifier_lookup
을 생성하고 소멸자에서 이전 상태를 복원합니다. -
runtime_context
에 추가된 RAII 클래스로get_scope
멤버 함수에 의해 반환됩니다. 이 함수는 스택 크기를 생성자에 저장하고 소멸자에 복원합니다. - 나는 일반적으로
const
키워드와 상수 객체를 제거했습니다. 그것들은 유용할 수 있지만 절대적으로 필요한 것은 아닙니다. -
var
키워드는 현재 전혀 필요하지 않으므로 제거되었습니다. - 런타임에 배열 크기를 확인하는
sizeof
키워드를 추가했습니다. C++sizeof
가 컴파일 타임에 실행되기 때문에 일부 C++ 프로그래머는 이름 선택이 신성모독이라고 생각할 수도 있지만, 저는 일부 공통 변수 이름(예:size
)과의 충돌을 피하기 위해 해당 키워드를 선택했습니다. - 명시적으로 모든 것을
string
으로 변환하는tostring
키워드를 추가했습니다. 함수 오버로딩을 허용하지 않기 때문에 함수가 될 수 없습니다. - 다양하고 덜 흥미로운 변경 사항.
통사론
우리는 C 및 관련 프로그래밍 언어와 매우 유사한 구문을 사용하기 때문에 명확하지 않을 수 있는 세부 사항만 알려 드리겠습니다.
변수 유형 선언은 다음과 같습니다.
-
void
, 함수 반환 유형에만 사용됨 -
number
-
string
-
T[]
는T
유형의 요소를 보유하는 배열입니다. -
R(P1,...,Pn)
은R
유형을 반환하고P1
~Pn
유형의 인수를 수신하는 함수입니다. 참조로 전달되는 경우 각 유형에는&
가 접두사로 붙을 수 있습니다.
함수 선언은 다음과 같습니다. [public] function R name(P1 p1, … Pn pn)
따라서 접두어가 function
이어야 합니다. public
접두어가 있으면 C++에서 호출할 수 있습니다. 함수가 값을 반환하지 않으면 반환 유형의 기본값으로 평가됩니다.
첫 번째 표현식에서 선언과 함께 for
루프를 허용합니다. 또한 C++17에서와 같이 초기화 표현식을 사용하여 if
-statement 및 switch
-statement를 허용합니다. if
문은 if
블록으로 시작하고 그 뒤에 0개 이상의 elif
블록이 오고 선택적으로 하나의 else
블록이 옵니다. 변수가 if
문의 초기화 식에서 선언된 경우 해당 블록 각각에서 볼 수 있습니다.
여러 중첩 루프에서 중단될 수 있는 break
문 뒤에 선택적 숫자를 허용합니다. 따라서 다음 코드를 가질 수 있습니다.
for (number i = 0; i < 100; ++i) { for(number j = 0; j < 100; ++j) { if (rnd(100) == 0) { break 2; } } }
또한 두 루프에서 모두 중단됩니다. 그 숫자는 컴파일 타임에 검증됩니다. 얼마나 멋진가요?
컴파일러
이 부분에 많은 기능이 추가되었지만 너무 자세히 설명하면 여전히 나와 함께하는 가장 끈질긴 독자조차 잃게 될 것입니다. 그러므로 나는 이야기의 매우 큰 부분인 편집을 의도적으로 건너뛸 것입니다.
그것은 이미 이 블로그 시리즈의 첫 번째 부분과 두 번째 부분에서 설명했기 때문입니다. 표현 위주로 해봤는데 다른 걸 모아보니 별반 다를게 없네요.
그러나 한 가지 예를 들겠습니다. 이 코드 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)); }
보시다시피 복잡하지 않습니다. while
, then (
을 구문 분석한 다음 숫자 표현식을 작성하고(우리는 부울이 없음) 구문 분석 )
.
그런 다음 {
및 }
내부에 있을 수 있는 블록 문을 컴파일하고(예, 단일 문 블록을 허용했습니다) 결국 while
문을 생성합니다.
처음 두 함수 인수는 이미 익숙합니다. 세 번째 possible_flow
는 구문 분석 중인 컨텍스트에서 허용된 흐름 변경 명령( continue
, break
, return
)을 보여줍니다. 컴파일 문이 일부 compiler
클래스의 멤버 함수인 경우 해당 정보를 개체에 보관할 수 있지만 저는 매머드 클래스의 열렬한 팬이 아니며 컴파일러는 확실히 그러한 클래스 중 하나일 것입니다. 추가 인수, 특히 얇은 인수를 전달하는 것은 누구에게도 해를 끼치지 않으며 언젠가는 우리가 코드를 병렬화할 수 있을 것입니다.
여기에서 설명하고 싶은 편집의 또 다른 흥미로운 측면이 있습니다.
두 함수가 서로를 호출하는 시나리오를 지원하려면 C 방식으로 수행할 수 있습니다. 전방 선언을 허용하거나 두 개의 컴파일 단계를 사용하는 것입니다.
저는 두 번째 방법을 선택했습니다. 함수 정의를 찾으면 해당 유형과 이름을 incomplete_function
이라는 객체로 구문 분석합니다. 그런 다음 첫 번째 중괄호를 닫을 때까지 중괄호의 중첩 수준을 계산하여 해석 없이 본문을 건너뜁니다. 프로세스에서 토큰을 수집하고 incomplete_function
에 보관하고 함수 식별자를 compiler_context
에 추가합니다.
전체 파일을 전달하면 런타임에 호출될 수 있도록 각 함수를 완전히 컴파일합니다. 그렇게 하면 각 함수는 파일의 다른 함수를 호출할 수 있고 모든 전역 변수에 액세스할 수 있습니다.
전역 변수는 동일한 함수를 호출하여 초기화할 수 있으며, 이는 해당 함수가 초기화되지 않은 변수에 액세스하자마자 오래된 "닭과 달걀" 문제로 즉시 이어집니다.
그런 일이 발생하면 runtime_exception
을 던지면 문제가 해결됩니다. 그건 단지 제가 친절하기 때문입니다. Franky, 액세스 위반은 그러한 코드를 작성하는 것에 대한 처벌로 받을 수 있는 최소한의 것입니다.
글로벌 범위
전역 범위에 나타날 수 있는 엔터티에는 두 가지 종류가 있습니다.
- 전역 변수
- 기능
각 전역 변수는 올바른 유형을 반환하는 표현식으로 초기화할 수 있습니다. 이니셜라이저는 각 전역 변수에 대해 생성됩니다.
각 이니셜라이저는 lvalue
를 반환하므로 전역 변수의 생성자 역할을 합니다. 전역 변수에 대한 표현식이 제공되지 않으면 기본 이니셜라이저가 생성됩니다.
이것은 runtime_context
의 initialize
멤버 함수입니다.
void runtime_context::initialize() { _globals.clear(); for (const auto& initializer : _initializers) { _globals.emplace_back(initializer->evaluate(*this)); } }
생성자에서 호출됩니다. runtime_context
상태를 재설정하기 위해 명시적으로 호출할 수 있는 전역 변수 컨테이너를 지웁니다.
앞서 언급했듯이 초기화되지 않은 전역 변수에 액세스하는지 확인해야 합니다. 따라서 이것은 전역 변수 접근자입니다.
variable_ptr& runtime_context::global(int idx) { runtime_assertion( idx < _globals.size(), "Uninitialized global variable access" ); return _globals[idx]; }
첫 번째 인수가 false
로 평가되면 runtime_assertion
은 해당 메시지와 함께 runtime_error
를 발생시킵니다.
각 함수는 단일 명령문을 캡처하는 람다로 구현되며, 이는 함수가 수신하는 runtime_context
로 평가됩니다.
기능 범위
while
문 컴파일에서 볼 수 있듯이 컴파일러는 전체 함수의 블록을 나타내는 block 문으로 시작하여 재귀적으로 호출됩니다.
다음은 모든 문에 대한 추상 기본 클래스입니다.
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; };
기본 기능을 제외한 유일한 기능은 runtime_context
에서 명령문 논리를 수행하고 프로그램 논리가 다음에 갈 위치를 결정하는 flow
을 반환하는 execute
입니다.
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(); };
정적 생성자 함수는 자체 설명이 가능하며 0이 아닌 break_level
및 flow_type::f_break
유형이 다른 비논리적 flow
을 방지하기 위해 작성했습니다.
이제, consume_break
는 중단 수준이 하나 더 적은 중단 흐름을 생성하거나 중단 수준이 0에 도달하면 일반 흐름을 생성합니다.
이제 모든 문 유형을 확인합니다.
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(); } };
여기서 simple_statement
는 표현식에서 생성된 명령문입니다. 모든 표현식은 void
를 반환하는 표현식으로 컴파일할 수 있으므로 이로부터 simple_statement
를 생성할 수 있습니다. break
도, continue
도, return
도 표현식의 일부가 될 수 simple_statement
는 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
는 문의 std::vector
를 유지합니다. 하나씩 실행합니다. 각각이 비정상 흐름을 반환하면 해당 흐름을 즉시 반환합니다. RAII 범위 개체를 사용하여 지역 범위 변수 선언을 허용합니다.
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
는 지역 변수를 생성하고 새로운 지역 변수를 스택에 푸시하는 표현식을 평가합니다.
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
에는 컴파일 시간에 평가된 중단 수준이 있습니다. 해당 중단 수준에 해당하는 흐름만 반환합니다.
class continue_statement: public statement { public: continue_statement() = default; flow execute(runtime_context&) override { return flow::continue_flow(); } };
continue_statement
는 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
및 return_void_statement
둘 다 flow::return_flow()
를 반환합니다. 유일한 차이점은 전자에는 반환하기 전에 반환 값으로 평가되는 표현식이 있다는 것입니다.
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
블록, 0개 이상의 elif
블록 및 하나의 else
블록(비어 있을 수 있음)에 대해 생성된 if_statement
는 하나의 표현식이 1
로 평가될 때까지 각 표현식을 평가합니다. 그런 다음 해당 블록을 실행하고 실행 결과를 반환합니다. 1
로 평가되는 표현식이 없으면 마지막( else
) 블록의 실행을 반환합니다.

if_declare_statement
는 if 절의 첫 번째 부분으로 선언이 있는 문입니다. 선언된 모든 변수를 스택에 푸시한 다음 기본 클래스( 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
는 명령문을 하나씩 실행하지만 먼저 표현식 평가에서 가져온 적절한 인덱스로 점프합니다. 명령문이 비정상 흐름을 반환하는 경우 해당 흐름을 즉시 반환합니다. flow_type::f_break
가 있으면 먼저 한 번의 중단을 사용합니다.
switch_declare_statement
는 헤더에 선언을 허용합니다. 그 중 어느 것도 본문에서 선언을 허용하지 않습니다.
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
와 do_while_statement
는 모두 표현이 1
로 평가되는 동안 본문 문을 실행합니다. 실행이 flow_type::f_break
를 반환하면 이를 소비하고 반환합니다. flow_type::f_return
을 반환하면 반환합니다. 정상적인 실행 또는 계속의 경우 아무 작업도 수행하지 않습니다.
continue
이 효과가 없는 것처럼 보일 수 있습니다. 그러나 내부 진술은 영향을 받았습니다. 예를 들어 block_statement
인 경우 끝까지 평가되지 않았습니다.
나는 while_statement
가 C++ while
로 구현되고 do-statement
가 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
및 for_statement_declare
는 while_statement
및 do_statement
와 유사하게 구현됩니다. 대부분의 논리를 수행하는 for_statement_base
클래스에서 상속됩니다. for
루프의 첫 번째 부분이 변수 선언일 때 for_statement_declare
가 생성됩니다.

이것들은 모두 우리가 가지고 있는 명령문 클래스입니다. 그것들은 우리 기능의 빌딩 블록입니다. runtime_context
가 생성되면 해당 기능을 유지합니다. 함수가 키워드 public
으로 선언되면 이름으로 호출할 수 있습니다.
이것으로 Stork의 핵심 기능을 마칩니다. 내가 설명할 다른 모든 것은 우리 언어를 더 유용하게 만들기 위해 추가한 사후 생각입니다.
튜플
배열은 단일 유형의 요소만 포함할 수 있으므로 동종 컨테이너입니다. 이기종 컨테이너를 원하면 구조가 즉시 떠오릅니다.
그러나 더 사소한 이기종 컨테이너인 튜플이 있습니다. 튜플은 다른 유형의 요소를 유지할 수 있지만 해당 유형은 컴파일 시간에 알려야 합니다. 다음은 Stork의 튜플 선언 예입니다.
[number, string] t = {22321, "Siveric"};
number
와 string
의 쌍을 선언하고 초기화합니다.
초기화 목록은 배열을 초기화하는 데에도 사용할 수 있습니다. 초기화 목록의 표현식 유형이 변수 유형과 일치하지 않으면 컴파일러 오류가 발생합니다.
배열이 variable_ptr
의 컨테이너로 구현되기 때문에 무료로 튜플의 런타임 구현을 얻었습니다. 포함된 변수의 올바른 유형을 확인하는 것은 컴파일 시간입니다.
모듈
Stork 사용자에게 구현 세부 정보를 숨기고 보다 사용자 친화적인 방식으로 언어를 제시하는 것이 좋을 것입니다.
이를 달성하는 데 도움이 되는 수업입니다. 구현 세부 정보 없이 제시합니다.
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
및 try_load
함수는 지정된 경로에서 Stork 스크립트를 로드하고 컴파일합니다. 첫째, 그들 중 하나는 stork::error
를 던질 수 있지만, 두 번째 것은 그것을 잡아서 출력에 출력할 것입니다(제공된 경우).
reset_globals
함수는 전역 변수를 다시 초기화합니다.
add_external_functions
및 create_public_function_caller
함수는 컴파일 전에 호출되어야 합니다. 첫 번째는 Stork에서 호출할 수 있는 C++ 함수를 추가합니다. 두 번째 것은 C++에서 Stork 함수를 호출하는 데 사용할 수 있는 호출 가능한 개체를 만듭니다. Stork 스크립트 컴파일 중에 공용 함수 유형이 R(Args…)
과 일치하지 않으면 컴파일 시간 오류가 발생합니다.
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);
예시
다음은 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)); }
다음은 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; }
표준 함수는 컴파일 전에 모듈에 추가되고 함수 trace
및 rnd
는 Stork 스크립트에서 사용됩니다. greater
기능도 쇼케이스로 추가됩니다.
스크립트는 "main.cpp"와 동일한 폴더에 있는 "test.stk" 파일에서 로드되고( __FILE__
전처리기 정의를 사용하여) main
함수가 호출됩니다.
스크립트에서 무작위 배열을 생성하여 비교기 less
를 사용하여 오름차순으로 정렬한 다음 C++로 작성된 비교기 greater
를 사용하여 내림차순으로 정렬합니다.
C(또는 C에서 파생된 모든 프로그래밍 언어)에 능통한 사람이라면 누구나 코드를 완벽하게 읽을 수 있습니다.
다음에 할일?
Stork에서 구현하고 싶은 많은 기능이 있습니다.
- 구조
- 클래스와 상속
- 모듈 간 호출
- 람다 함수
- 동적으로 유형이 지정된 개체
시간과 공간의 부족은 아직 구현하지 않은 이유 중 하나입니다. 여가 시간에 새 기능을 구현하면서 GitHub 페이지를 새 버전으로 업데이트하려고 합니다.
마무리
우리는 새로운 프로그래밍 언어를 만들었습니다!
지난 6주 동안 내 여가 시간의 상당 부분을 차지했지만 이제 일부 스크립트를 작성하고 실행되는 것을 볼 수 있습니다. 그것이 내가 지난 며칠 동안 예기치 않게 충돌 할 때마다 대머리를 긁는 일이었습니다. 때로는 작은 버그, 때로는 고약한 버그였습니다. 하지만 한편으로는 이미 세상과 공유한 잘못된 결정에 대한 것이기 때문에 부끄럽기도 했습니다. 하지만 매번 수정하고 코딩을 계속했습니다.
그 과정에서 한 번도 사용해본 적이 없는 if constexpr
에 대해 배웠습니다. 나는 또한 rvalue-references 및 Perfect forwarding 뿐만 아니라 내가 매일 접하지 않는 C++17의 다른 작은 기능들에 더 익숙해졌습니다.
코드는 완벽하지 않습니다. 그런 주장은 절대 하지 않겠습니다. 하지만 충분히 훌륭하고 대부분 좋은 프로그래밍 방식을 따릅니다. 그리고 가장 중요한 것은 작동합니다.
새로운 언어를 처음부터 개발하기로 결정하는 것은 평범한 사람이나 심지어 평범한 프로그래머에게는 미친 소리처럼 들릴 수 있지만, 그렇게 하고 할 수 있다는 것을 스스로 증명해야 하는 더 큰 이유입니다. 어려운 퍼즐을 푸는 것과 마찬가지로 정신적 건강을 유지하는 데 좋은 두뇌 운동입니다.
지루한 과제는 일상적인 프로그래밍에서 흔히 볼 수 있습니다. 흥미로운 부분만 골라서 따를 수 없고 때때로 지루하더라도 진지한 작업을 해야 하기 때문입니다. 전문 개발자라면 가장 먼저 해야 할 일은 고품질 코드를 고용주에게 전달하고 음식을 식탁에 올려놓는 것입니다. 이것은 때때로 여가 시간에 프로그래밍을 피하게 만들고 초기 프로그래밍 학교 시절의 열정을 약화시킬 수 있습니다.
그럴 필요가 없다면 그 열정을 잃지 마세요. 이미 완료되었더라도 흥미롭게 생각하면 작업하십시오. 재미를 위해 이유를 정당화할 필요는 없습니다.
그리고 그것을 부분적으로라도 전문 업무에 통합할 수 있다면 도움이 될 것입니다! 그런 기회가 있는 사람은 많지 않습니다.
이 부분의 코드는 내 GitHub 페이지의 전용 분기로 고정됩니다.