Proposal: Improvements to APIs¶
Note
Feature awaiting further design consideration.
This proposal suggests some simplifications to how APIs are described.
Rationale¶
- Currently we have too much indentation for export/import.
- Namespaces add further indentation and verbosity.
- It would be simpler not to have multiple API descriptions/implementations per source file.
Design¶
The key points of the design are:
- New keywords: api, implement and depends.
- Every file must be describing or implementing an API. This is expressed on the first line (either api ... or implement api ...).
- namespace is removed; modules can reference the api name if needed to resolve clashes.
API description¶
As an example, here is how the API description for TCP streams might look:
api std::tcp 0.1.0 depends {
primitives 0.1.0,
std::event 0.1.0,
std::string 0.1.0
}
exception socket_error(string error);
interface in_buffer {
const uint8_t* data() const;
size_t size() const;
}
interface out_buffer {
uint8_t* data();
size_t size() const;
}
class tcp_stream {
event_source all_events() const noexcept;
event_source read_events() const noexcept;
event_source write_events() const noexcept;
bool valid() const noexcept;
endpoint peer() const noexcept;
size_t read_some(out_buffer& destBuffer);
size_t write_some(const in_buffer& sourceBuffer);
}
The key aspects are:
- The file must start with api <name> <version>.
- Each dependency must be specified with import <name> <version>;. The dependencies themselves are specified in other source files.
- Each exported function/type must use export.
API implementation¶
Here’s how the API implementation for varray might look:
implement api std::container 0.1.0;
import std::memory 0.1.0;
/**
* \brief Resizable array.
*/
template <sized_type T>
export class varray(size_t size, size_t capacity, T* data) {
static create() noexcept {
return @(0u, 0u, null);
}
...
}
The key aspects are:
- The file must start with implement api <name> <version>.
- Each dependency must be specified with import <name> <version>;. The dependencies themselves are specified in other source files.
- Each exported function/type must use export.
We use implement api ... rather than just implement ... to give a clear attachment between this code and the associated api ... (in another file).
Main function¶
The main function would be in a special unnamed API:
import std::{io, string} 0.1.0;
export int main(unused int argc, unused ubyte ** argv) {
println("test!");
return 0;
}
Assigning imports to namespaces¶
With the removal of namespace, there could be clashes between imported names. A module can fix this by explicitly referencing the API it wants:
implement api test 0.1.0;
import first_api 0.1.0;
import second_api 0.1.0;
export void f() {
first_api::f();
second_api::f();
// For a specific version.
first_api@0.1.0::f();
}
Importing API that depends on other API¶
Importing an API means that its dependencies would not also be imported:
implement api test 0.1.0;
import drive_car_api 0.1.0;
export void f() {
// ERROR: 'car_api' not imported.
drive_car(car());
}
This helps to avoid unnecessary imports (and hence name clashes). It can be fixed by adding the relevant import:
implement api test 0.1.0;
import car_api 0.1.0;
import drive_car_api 0.1.0;
export void f() {
// OK
drive_car(car());
}
Dependencies vs implementation imports¶
depends indicates dependencies of the API, whereas import ... in an implementation is a dependency of the implementation. A dependency of an API implies a dependency of the implementation, but not vice versa.
The compiler should issue warnings for dependencies that aren’t required by the API:
implement api test 0.1.0 depends {
// ERROR: no constructs within 'car_api' are exposed by 'test'
car_api 0.1.0
}
import drive_car_api 0.1.0;
export void f() {
// OK
drive_car(car());
}
The compiler should also issue warnings for imports that aren’t by the implementation:
implement api test 0.1.0;
import car_api 0.1.0;
import drive_car_api 0.1.0;
// ERROR: no constructs within 'blah' are used by 'test'
import blah 0.1.0;
export void f() {
// OK
drive_car(car());
}
Interacting with other languages¶
With C¶
Loci would able to inport C headers directly:
implement api test 0.1.0;
import c::memory 1.0.0: lang(c) <memory.h>;
export void f() {
(void) malloc(10);
}
Internally the compiler would produce something like:
api c::memory 1.0.0;
void* malloc(size_t size);
// etc..
With C++¶
Similarly for C++:
implement api test 0.1.0;
import c++::vector 1.0.0: lang(c++) <vector>;
export void f() {
auto array = std::vector<int>();
}
Internally the compiler would produce something like:
api c++::vector 1.0.0;
template <typename T>
class std::vector {
// etc.
}
This proposal changes the syntax of APIs to now use :: rather than . to make this more seamless.