Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement the targets clause on the package directive. #33

Merged
merged 2 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LANGUAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ statement ::= import-statement
| let-statement
| export-statement

package-decl ::= `package` package-name `;`
package-decl ::= `package` package-name (`targets` package-path)? `;`
package-name ::= id (':' id)+ ('@' version)?
version ::= <SEMVER>

Expand Down
36 changes: 27 additions & 9 deletions crates/wac-parser/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,10 @@ pub(crate) trait Parse<'a>: Sized {
fn parse(lexer: &mut Lexer<'a>) -> ParseResult<Self>;
}

trait Peek {
fn peek(lookahead: &mut Lookahead) -> bool;
}

fn parse_delimited<'a, T: Parse<'a> + Peek>(
lexer: &mut Lexer<'a>,
until: Token,
Expand Down Expand Up @@ -288,8 +292,25 @@ fn parse_delimited<'a, T: Parse<'a> + Peek>(
Ok(items)
}

trait Peek {
fn peek(lookahead: &mut Lookahead) -> bool;
/// Represents a package directive in the AST.
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct PackageDirective<'a> {
/// The name of the package named by the directive.
pub package: PackageName<'a>,
/// The optional world being targeted by the package.
#[serde(skip_serializing_if = "Option::is_none")]
pub targets: Option<PackagePath<'a>>,
}

impl<'a> Parse<'a> for PackageDirective<'a> {
fn parse(lexer: &mut Lexer<'a>) -> ParseResult<Self> {
parse_token(lexer, Token::PackageKeyword)?;
let package = Parse::parse(lexer)?;
let targets = parse_optional(lexer, Token::TargetsKeyword, Parse::parse)?;
parse_token(lexer, Token::Semicolon)?;
Ok(Self { package, targets })
}
}

/// Represents a top-level WAC document.
Expand All @@ -298,8 +319,8 @@ trait Peek {
pub struct Document<'a> {
/// The doc comments for the package.
pub docs: Vec<DocComment<'a>>,
/// The package name of the document.
pub package: PackageName<'a>,
/// The package directive of the document.
pub directive: PackageDirective<'a>,
/// The statements in the document.
pub statements: Vec<Statement<'a>>,
}
Expand All @@ -312,10 +333,7 @@ impl<'a> Document<'a> {
let mut lexer = Lexer::new(source).map_err(Error::from)?;

let docs = Parse::parse(&mut lexer)?;

parse_token(&mut lexer, Token::PackageKeyword)?;
let package = Parse::parse(&mut lexer)?;
parse_token(&mut lexer, Token::Semicolon)?;
let directive = Parse::parse(&mut lexer)?;

let mut statements: Vec<Statement> = Default::default();
while lexer.peek().is_some() {
Expand All @@ -325,7 +343,7 @@ impl<'a> Document<'a> {
assert!(lexer.next().is_none(), "expected all tokens to be consumed");
Ok(Self {
docs,
package,
directive,
statements,
})
}
Expand Down
25 changes: 19 additions & 6 deletions crates/wac-parser/src/ast/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,7 @@ impl<'a, W: Write> DocumentPrinter<'a, W> {
/// Prints the given document.
pub fn document(&mut self, doc: &Document) -> std::fmt::Result {
self.docs(&doc.docs)?;
writeln!(
self.writer,
"package {package};",
package = self.source(doc.package.span),
)?;
self.newline()?;
self.package_directive(&doc.directive)?;

for (i, statement) in doc.statements.iter().enumerate() {
if i > 0 {
Expand All @@ -49,6 +44,24 @@ impl<'a, W: Write> DocumentPrinter<'a, W> {
Ok(())
}

/// Prints the given package directive.
pub fn package_directive(&mut self, directive: &PackageDirective) -> std::fmt::Result {
self.indent()?;
write!(
self.writer,
"package {package}",
package = self.source(directive.package.span),
)?;

if let Some(targets) = &directive.targets {
write!(self.writer, " ")?;
self.package_path(targets)?;
}

writeln!(self.writer, ";")?;
self.newline()
}

/// Prints the given statement.
pub fn statement(&mut self, statement: &Statement) -> std::fmt::Result {
match statement {
Expand Down
8 changes: 8 additions & 0 deletions crates/wac-parser/src/lexer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,9 @@ pub enum Token {
/// The `package` keyword.
#[token("package")]
PackageKeyword,
/// The `targets` keyword.
#[token("targets")]
TargetsKeyword,

/// The `;` symbol.
#[token(";")]
Expand Down Expand Up @@ -338,6 +341,7 @@ impl fmt::Display for Token {
Token::IncludeKeyword => write!(f, "`include` keyword"),
Token::AsKeyword => write!(f, "`as` keyword"),
Token::PackageKeyword => write!(f, "`package` keyword"),
Token::TargetsKeyword => write!(f, "`targets` keyword"),
Token::Semicolon => write!(f, "`;`"),
Token::OpenBrace => write!(f, "`{{`"),
Token::CloseBrace => write!(f, "`}}`"),
Expand Down Expand Up @@ -827,6 +831,8 @@ let
use
include
as
package
targets
"#,
&[
(Ok(Token::ImportKeyword), "import", 1..7),
Expand Down Expand Up @@ -866,6 +872,8 @@ as
(Ok(Token::UseKeyword), "use", 203..206),
(Ok(Token::IncludeKeyword), "include", 207..214),
(Ok(Token::AsKeyword), "as", 215..217),
(Ok(Token::PackageKeyword), "package", 218..225),
(Ok(Token::TargetsKeyword), "targets", 226..233),
],
);
}
Expand Down
49 changes: 49 additions & 0 deletions crates/wac-parser/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,44 @@ pub enum Error {
#[label(primary, "spreading the exports of this instance has no effect")]
span: SourceSpan,
},
/// An import is not in the target world.
#[error("import `{name}` is not present in target world `{world}`")]
ImportNotInTarget {
/// The import name.
name: String,
/// The target world.
world: String,
/// The span where the error occurred.
#[label(primary, "import `{name}` is not in the target world")]
span: SourceSpan,
},
/// Missing an export for the target world.
#[error("missing export `{name}` for target world `{world}`")]
MissingTargetExport {
/// The export name.
name: String,
/// The target world.
world: String,
/// The span where the error occurred.
#[label(primary, "must export `{name}` to target this world")]
span: SourceSpan,
},
/// An import or export has a mismatched type for the target world.
#[error("{kind} `{name}` has a mismatched type for target world `{world}`")]
TargetMismatch {
/// The kind of mismatch.
kind: ExternKind,
/// The mismatched extern name.
name: String,
/// The target world.
world: String,
/// The span where the error occurred.
#[label(primary, "mismatched type for {kind} `{name}`")]
span: SourceSpan,
/// The source of the error.
#[source]
source: anyhow::Error,
},
}

/// Represents a resolution result.
Expand Down Expand Up @@ -732,6 +770,17 @@ impl ItemKind {
ItemKind::Value(_) => "value",
}
}

/// Promote function types, instance types, and component types
/// to functions, instances, and components
fn promote(&self) -> Self {
match *self {
ItemKind::Type(Type::Func(id)) => ItemKind::Func(id),
ItemKind::Type(Type::Interface(id)) => ItemKind::Instance(id),
ItemKind::Type(Type::World(id)) => ItemKind::Component(id),
kind => kind,
}
}
}

/// Represents a item defining a type.
Expand Down
102 changes: 89 additions & 13 deletions crates/wac-parser/src/resolution/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,29 @@ impl<'a> AstResolver<'a> {
}
}

// If there's a target world in the directive, validate the composition
// conforms to the target
if let Some(path) = &self.document.directive.targets {
let item = self.resolve_package_export(&mut state, path)?;
match item {
ItemKind::Type(Type::World(world)) => {
self.validate_target(&state, path, world)?;
}
_ => {
return Err(Error::NotWorld {
name: path.string.to_owned(),
kind: item.as_str(&self.definitions).to_owned(),
span: path.span,
});
}
}
}

assert!(state.scopes.is_empty());

Ok(Composition {
package: self.document.package.name.to_owned(),
version: self.document.package.version.clone(),
package: self.document.directive.package.name.to_owned(),
version: self.document.directive.package.version.clone(),
definitions: self.definitions,
packages: state.packages,
items: state.current.items,
Expand Down Expand Up @@ -227,13 +245,8 @@ impl<'a> AstResolver<'a> {
ast::ImportType::Ident(id) => (state.local_item(id)?.1.kind(), stmt.id.span),
};

// Promote function types, instance types, and component types to functions, instances, and components
let kind = match kind {
ItemKind::Type(Type::Func(id)) => ItemKind::Func(id),
ItemKind::Type(Type::Interface(id)) => ItemKind::Instance(id),
ItemKind::Type(Type::World(id)) => ItemKind::Component(id),
_ => kind,
};
// Promote any types to their corresponding item kind
let kind = kind.promote();

let (name, span) = if let Some(name) = &stmt.name {
// Override the span to the `as` clause string
Expand Down Expand Up @@ -505,8 +518,8 @@ impl<'a> AstResolver<'a> {
fn id(&self, name: &str) -> String {
format!(
"{pkg}/{name}{version}",
pkg = self.document.package.name,
version = if let Some(version) = &self.document.package.version {
pkg = self.document.directive.package.name,
version = if let Some(version) = &self.document.directive.package.version {
format!("@{version}")
} else {
String::new()
Expand Down Expand Up @@ -1551,7 +1564,7 @@ impl<'a> AstResolver<'a> {
path: &'a ast::PackagePath<'a>,
) -> ResolutionResult<ItemKind> {
// Check for reference to local item
if path.name == self.document.package.name {
if path.name == self.document.directive.package.name {
return self.resolve_local_export(state, path);
}

Expand Down Expand Up @@ -1704,7 +1717,7 @@ impl<'a> AstResolver<'a> {
state: &mut State<'a>,
expr: &'a ast::NewExpr<'a>,
) -> ResolutionResult<ItemId> {
if expr.package.name == self.document.package.name {
if expr.package.name == self.document.directive.package.name {
return Err(Error::UnknownPackage {
name: expr.package.name.to_string(),
span: expr.package.span,
Expand Down Expand Up @@ -2248,4 +2261,67 @@ impl<'a> AstResolver<'a> {
aliases.insert(name.to_owned(), id);
Ok(Some(id))
}

fn validate_target(
&self,
state: &State,
path: &ast::PackagePath,
world: WorldId,
) -> ResolutionResult<()> {
let world = &self.definitions.worlds[world];

let mut checker = SubtypeChecker::new(&self.definitions, &state.packages);

// The output is allowed to import a subset of the world's imports
for (name, import) in &state.imports {
let expected = world
.imports
.get(name)
.ok_or_else(|| Error::ImportNotInTarget {
name: name.clone(),
world: path.string.to_owned(),
span: import.span,
})?;

checker
.is_subtype(
expected.promote(),
state.root_scope().items[import.item].kind(),
)
.map_err(|e| Error::TargetMismatch {
kind: ExternKind::Import,
name: name.clone(),
world: path.string.to_owned(),
span: import.span,
source: e,
})?;
}

// The output must export every export in the world
for (name, expected) in &world.exports {
let export = state
.exports
.get(name)
.ok_or_else(|| Error::MissingTargetExport {
name: name.clone(),
world: path.string.to_owned(),
span: path.span,
})?;

checker
.is_subtype(
expected.promote(),
state.root_scope().items[export.item].kind(),
)
.map_err(|e| Error::TargetMismatch {
kind: ExternKind::Export,
name: name.clone(),
world: path.string.to_owned(),
span: export.span,
source: e,
})?;
}

Ok(())
}
}
16 changes: 9 additions & 7 deletions crates/wac-parser/tests/parser/export.wac.result
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
{
"docs": [],
"package": {
"string": "test:comp",
"name": "test:comp",
"version": null,
"span": {
"offset": 8,
"length": 9
"directive": {
"package": {
"string": "test:comp",
"name": "test:comp",
"version": null,
"span": {
"offset": 8,
"length": 9
}
}
},
"statements": [
Expand Down
Loading