Skip to content

Commit

Permalink
Add support for interfacing with C libraries
Browse files Browse the repository at this point in the history
This adds support for linking against C libraries and using their types
and functions. The FFI is fairly strict and a bit limited, such as not
performing automatic type conversions, but this is a deliberate choice:
it keeps the compiler's complexity at a reasonable level, and it should
(hopefully) further drive home the idea that one should avoid
interfacing with C as much as they can, as all of Inko's safety
guarantees are thrown out of the window when doing so.

This fixes #290.

Changelog: added
  • Loading branch information
yorickpeterse committed Jun 28, 2023
1 parent 59578a1 commit cc15cf5
Show file tree
Hide file tree
Showing 42 changed files with 2,597 additions and 2,269 deletions.
5 changes: 5 additions & 0 deletions ast/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ pub enum TokenKind {
UnsignedShrAssign,
While,
Whitespace,
Extern,
}

impl TokenKind {
Expand Down Expand Up @@ -274,6 +275,7 @@ impl TokenKind {
TokenKind::Recover => "the 'recover' keyword",
TokenKind::Nil => "the 'nil' keyword",
TokenKind::Replace => "a '=:'",
TokenKind::Extern => "the 'extern' keyword",
}
}
}
Expand Down Expand Up @@ -340,6 +342,7 @@ impl Token {
| TokenKind::False
| TokenKind::Case
| TokenKind::Enum
| TokenKind::Extern
)
}

Expand Down Expand Up @@ -995,6 +998,7 @@ impl Lexer {
"import" => TokenKind::Import,
"return" => TokenKind::Return,
"static" => TokenKind::Static,
"extern" => TokenKind::Extern,
_ => TokenKind::Identifier,
},
7 => match value.as_str() {
Expand Down Expand Up @@ -2073,6 +2077,7 @@ mod tests {
assert_token!("import", Import, "import", 1..=1, 1..=6);
assert_token!("return", Return, "return", 1..=1, 1..=6);
assert_token!("static", Static, "static", 1..=1, 1..=6);
assert_token!("extern", Extern, "extern", 1..=1, 1..=6);

assert_token!("builtin", Builtin, "builtin", 1..=1, 1..=7);
assert_token!("recover", Recover, "recover", 1..=1, 1..=7);
Expand Down
22 changes: 22 additions & 0 deletions ast/src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,24 @@ impl Node for Import {
}
}

#[derive(Debug, PartialEq, Eq)]
pub struct ExternImportPath {
pub path: String,
pub location: SourceLocation,
}

#[derive(Debug, PartialEq, Eq)]
pub struct ExternImport {
pub path: ExternImportPath,
pub location: SourceLocation,
}

impl Node for ExternImport {
fn location(&self) -> &SourceLocation {
&self.location
}
}

#[derive(Debug, PartialEq, Eq)]
pub struct DefineConstant {
pub public: bool,
Expand All @@ -359,6 +377,7 @@ pub enum MethodKind {
Moving,
Mutable,
AsyncMutable,
Extern,
}

#[derive(Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -419,6 +438,7 @@ pub enum ClassKind {
Builtin,
Enum,
Regular,
Extern,
}

#[derive(Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -501,6 +521,7 @@ pub enum TopLevelExpression {
ReopenClass(Box<ReopenClass>),
ImplementTrait(Box<ImplementTrait>),
Import(Box<Import>),
ExternImport(Box<ExternImport>),
}

impl Node for TopLevelExpression {
Expand All @@ -513,6 +534,7 @@ impl Node for TopLevelExpression {
TopLevelExpression::ReopenClass(ref typ) => typ.location(),
TopLevelExpression::ImplementTrait(ref typ) => typ.location(),
TopLevelExpression::Import(ref typ) => typ.location(),
TopLevelExpression::ExternImport(ref typ) => typ.location(),
}
}
}
Expand Down
208 changes: 199 additions & 9 deletions ast/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ impl Parser {
&mut self,
start: Token,
) -> Result<TopLevelExpression, ParseError> {
if self.peek().kind == TokenKind::Extern {
return self.extern_import(start);
}

let path = self.import_path()?;
let symbols = self.import_symbols()?;
let end_loc =
Expand Down Expand Up @@ -239,6 +243,48 @@ impl Parser {
Ok(Some(ImportAlias { name: token.value, location: token.location }))
}

fn extern_import(
&mut self,
start: Token,
) -> Result<TopLevelExpression, ParseError> {
// Skip the "extern".
self.next();

let path_start = self.require()?;
let path = self.extern_import_path(path_start)?;
let location =
SourceLocation::start_end(&start.location, &path.location);

Ok(TopLevelExpression::ExternImport(Box::new(ExternImport {
path,
location,
})))
}

fn extern_import_path(
&mut self,
start: Token,
) -> Result<ExternImportPath, ParseError> {
let close = match start.kind {
TokenKind::SingleStringOpen => TokenKind::SingleStringClose,
TokenKind::DoubleStringOpen => TokenKind::DoubleStringClose,
_ => {
error!(
start.location,
"expected a single or double quote, found '{}' instead",
start.kind.description()
);
}
};

let text = self.expect(TokenKind::StringText)?;
let close = self.expect(close)?;
let location =
SourceLocation::start_end(&start.location, &close.location);

Ok(ExternImportPath { path: text.value, location })
}

fn define_constant(
&mut self,
start: Token,
Expand Down Expand Up @@ -715,16 +761,38 @@ impl Parser {
start: Token,
) -> Result<TopLevelExpression, ParseError> {
let public = self.next_is_public();
let kind = MethodKind::Instance;
let kind = match self.peek().kind {
TokenKind::Extern => {
self.next();
MethodKind::Extern
}
_ => MethodKind::Instance,
};

let name_token = self.require()?;
let (name, operator) = self.method_name(name_token)?;
let type_parameters = self.optional_type_parameter_definitions()?;
let type_parameters = if let MethodKind::Extern = kind {
None
} else {
self.optional_type_parameter_definitions()?
};
let arguments = self.optional_method_arguments()?;
let return_type = self.optional_return_type()?;
let body_token = self.expect(TokenKind::CurlyOpen)?;
let body = self.expressions(body_token)?;
let location =
SourceLocation::start_end(&start.location, &body.location);
let body = if let MethodKind::Extern = kind {
None
} else {
let token = self.expect(TokenKind::CurlyOpen)?;

Some(self.expressions(token)?)
};

let location = SourceLocation::start_end(
&start.location,
location!(body)
.or_else(|| location!(return_type))
.or_else(|| location!(arguments))
.unwrap_or_else(|| &name.location),
);

Ok(TopLevelExpression::DefineMethod(Box::new(DefineMethod {
public,
Expand All @@ -734,7 +802,7 @@ impl Parser {
arguments,
return_type,
location,
body: Some(body),
body,
kind,
})))
}
Expand Down Expand Up @@ -884,11 +952,26 @@ impl Parser {
self.next();
ClassKind::Builtin
}
TokenKind::Extern => {
self.next();
ClassKind::Extern
}
_ => ClassKind::Regular,
};

let name = Constant::from(self.expect(TokenKind::Constant)?);
let type_parameters = self.optional_type_parameter_definitions()?;
let body = self.class_expressions()?;
let type_parameters = if let ClassKind::Extern = kind {
None
} else {
self.optional_type_parameter_definitions()?
};

let body = if let ClassKind::Extern = kind {
self.extern_class_expressions()?
} else {
self.class_expressions()?
};

let location =
SourceLocation::start_end(&start.location, &body.location);

Expand Down Expand Up @@ -944,6 +1027,38 @@ impl Parser {
}
}

fn extern_class_expressions(
&mut self,
) -> Result<ClassExpressions, ParseError> {
let start = self.expect(TokenKind::CurlyOpen)?;
let mut values = Vec::new();

loop {
let token = self.require()?;

if token.kind == TokenKind::CurlyClose {
let location =
SourceLocation::start_end(&start.location, &token.location);

return Ok(ClassExpressions { values, location });
}

let node = match token.kind {
TokenKind::Let => ClassExpression::DefineField(Box::new(
self.define_field(token)?,
)),
_ => {
error!(
token.location,
"Expected a 'let', found '{}' instead", token.value
);
}
};

values.push(node);
}
}

fn class_expression(
&mut self,
start: Token,
Expand Down Expand Up @@ -2917,6 +3032,7 @@ mod tests {
Parser::new(input.into(), "test.inko".into())
}

#[track_caller]
fn parse(input: &str) -> Module {
parser(input)
.parse()
Expand Down Expand Up @@ -3083,6 +3199,34 @@ mod tests {
);
}

#[test]
fn test_extern_imports() {
assert_eq!(
top(parse("import extern 'foo'")),
TopLevelExpression::ExternImport(Box::new(ExternImport {
path: ExternImportPath {
path: "foo".to_string(),
location: cols(15, 19)
},
location: cols(1, 19)
}))
);

assert_eq!(
top(parse("import extern \"foo\"")),
TopLevelExpression::ExternImport(Box::new(ExternImport {
path: ExternImportPath {
path: "foo".to_string(),
location: cols(15, 19)
},
location: cols(1, 19)
}))
);

assert_error!("import extern ''", cols(16, 16));
assert_error!("import extern \"\"", cols(16, 16));
}

#[test]
fn test_imports_with_symbols() {
assert_eq!(
Expand Down Expand Up @@ -4106,6 +4250,27 @@ mod tests {
);
}

#[test]
fn test_extern_method() {
assert_eq!(
top(parse("fn extern foo")),
TopLevelExpression::DefineMethod(Box::new(DefineMethod {
public: false,
operator: false,
kind: MethodKind::Extern,
name: Identifier {
name: "foo".to_string(),
location: cols(11, 13)
},
type_parameters: None,
arguments: None,
return_type: None,
body: None,
location: cols(1, 13),
}))
);
}

#[test]
fn test_invalid_methods() {
assert_error!("fn foo [ {}", cols(10, 10));
Expand All @@ -4115,6 +4280,7 @@ mod tests {
assert_error!("fn foo -> {}", cols(11, 11));
assert_error!("fn foo {", cols(8, 8));
assert_error!("fn foo", cols(6, 6));
assert_error!("fn extern foo[T](arg: T)", cols(14, 14));
}

#[test]
Expand Down Expand Up @@ -4158,6 +4324,28 @@ mod tests {
);
}

#[test]
fn test_extern_class() {
assert_eq!(
top(parse("class extern A {}")),
TopLevelExpression::DefineClass(Box::new(DefineClass {
public: false,
name: Constant {
source: None,
name: "A".to_string(),
location: cols(14, 14)
},
kind: ClassKind::Extern,
type_parameters: None,
body: ClassExpressions {
values: Vec::new(),
location: cols(16, 17)
},
location: cols(1, 17)
}))
);
}

#[test]
fn test_class_literal() {
assert_eq!(
Expand Down Expand Up @@ -4681,6 +4869,8 @@ mod tests {
assert_error!("class A { 10 }", cols(11, 12));
assert_error!("class {}", cols(7, 7));
assert_error!("class A {", cols(9, 9));
assert_error!("class extern A[T] {", cols(15, 15));
assert_error!("class extern A { fn foo { } }", cols(18, 19));
}

#[test]
Expand Down
Loading

0 comments on commit cc15cf5

Please sign in to comment.