bindings = korka::make_bindings(
"func", func,
"foo", foo
);
```
But why couldn't I make it work?
In C++ you can't pass a string into `template <auto ...args>`. We need `const_string`. We can't mix types in the one stream of variadic args and make compiler guess it right. Templates require explicitness and it's impossible to write a universal parser.
Variant #2 works, but you can't extract the function into the runtime. You just can't. Functions may have different signatures, but you need to make them all the same type, and create a FFI wrapper along the way. Compile-time doesn't allow `reinterpretet_cast<void*>(&func)`.
So I designed this:
```cpp
constexpr auto bindings = korka::make_bindings(
korka::wrap<fib>("cpp_fib"),
korka::wrap<print_n>("print_n")
);
```
Not so elegant, but still not bad.
`wrap` is very simple
```cpp
// our FFI signature
using vm_external_function_type = void(vm::context_base &context);
// info for our compiler
template<class Signature>
struct wrapped_function {
using signature_t = Signature;
vm_external_function_type &external_func;
std::string_view name;
};
template<auto func>
consteval auto wrap(std::string_view name) {
return wrapped_function<std::decay_t<decltype(func)>>{
binding_wrapper<func>,
name
};
}
```
The most interesting part is inside `binding_wrapper<func>`. I won't show the full code here, because I still didn't tell about the VM architecture that will execute it. But in short binding_wrapper just checks the signature, generates some code that extracts arguments from VM, calls native functions and then puts the result back. Simple.
## The compiler and the VM
Maybe the most interesting part of the article. I have never written any compilers before (the thing I mentioned in the beginning of the article doesn't count), so I made it according to the first articles I found in Google.
Compiler has 3 modules:
- the lexer - splitting the code into tokens,
- the parser - building a tree from the tokens,
- the compiler itself - making the tree into byte-code. And doing semantic analysis at the same time (I was too lazy to make another module)
I think I could compose everything into one class via composition or smth, but it's too late already.
### Lexer
Primitive. We just look for tokens in a loop until we reach EOF.
```cpp
constexpr auto scan_token() -> std::optional<std::expected<lex_token, error_t>> {
char c = advance();
switch (c) {
case '{':
return make_token(lex_kind::kOpenBrace);
case '}':
return make_token(lex_kind::kCloseBrace);
case '(':
return make_token(lex_kind::kOpenParenthesis);
case ')':
// ...
case ' ':
case '\r':
case '\t':
// Ignore whitespace
return std::nullopt;
// ...
default:
if (is_digit(c)) {
return scan_number();
} else if (is_alpha(c)) {
return scan_identifier();
}
}
}
```
### Parser
More interesting. We need to build the AST (abstract syntax tree). And we need to store this tree somehow. The usual way with `Node` that keeps pointers to other nodes won't do, because we're at compile-time. I mean, we can write it this way, it will work, but extracting this tree into compile time? No. We would need serialisation or something. So we can use simple trick with `std::vector<Node>` and just make nodes store indices to each other.
This approach also increases the cache locality of the data for the CPU, but I doubt the CPU will be even aware of our "smart" trick, since everything is executed at compile-time.
The parser is recursive, while parsing one expression we parse another. Small fragment of the code:
```cpp
constexpr auto parse_statement() -> parse_result {
auto tok = peek();
if (!tok) return make_error("Unexpected end of input");
switch (tok->kind) {
case lex_kind::kOpenBrace: return parse_compound_stmt(); // { ... }
case lex_kind::kIf: return parse_if_statement(); // if (...) ...
case lex_kind::kWhile: return parse_while_statement(); //
Post #25690
14