WAC is a superset of the WIT language.
However, unlike WIT, WAC is evaluated in a top-down fashion and is not a declarative language. Consequently, types must be declared before they are used in WAC.
In addition to being able to declare types, interfaces, and worlds, WAC also allows for defining a composition.
In the simplest terms, a composition is a collection of components that are instantiated in a topological order and certain exports from those instances are made available as exports of the composition itself.
Each WAC document must begin with a package
directive:
package example:composition;
The directive identifies the namespace and name of the package being defined.
Additionally, the package
directive may include a targets
clause:
package example:composition targets wasi:http/proxy;
The package path following a targets
clause must name a world. The parser
will then ensure that the composition is valid for the given world.
If the composition is not valid for the given world, then an error will be emitted.
WAC currently has three statements that extend the WIT language: import statements, let statements, and export statements.
A composition may explicitly import an item with the import
statement.
Assume a greeter
interface definition published to a registry:
package example:greeter;
interface greeter {
greet: func() -> string;
}
If we want to import an instance of this interface in our composition, we can
use the import
statement in WAC:
import greeter: example:greeter/greeter;
Items imported by a package path use the path as the name of the import in the
resulting composition; in the above example, the import name would be
example:greeter/greeter
.
The as
keyword can be used to rename the imported item:
import greeter as my-greeter: example:greeter/greeter;
Here the name of the import becomes my-greeter
; the name may be any valid
import name in the component model.
The local name greeter
can then be used to refer to the imported item from
the rest of the composition.
Imports may also use inline interface and function type declarations:
import greeter: interface {
greet: func() -> string;
};
import greet: func() -> string;
As these imports are not by package path, the name of the import will be the
same as the local name, by default. In the above example, this would be
greeter
and greet
, respectively. As previously mentioned, the as
keyword
can be used to specify a different import name.
While the import
statement is used to define an explicit import, the new
expression (covered below) can be used to implicitly import items from the
composition.
A special syntax of the new
expression allows for omitting instantiation
arguments in favor of simply importing the arguments directly from the
composition and passing them through to the instantiation.
Let's assume there exists a component named example:my-component
with the
following world (i.e. component type):
world my-component {
import i: interface {
f: func();
};
}
We can instantiate this component using the new
expression without having to
explicitly specify the i
argument:
let my-instance = new example:my-component { ... };
The special ...
syntax indicates that any arguments that were not specified
as part of the new
expression should be imported from the composition and
passed through to the instantiation. The ...
syntax must be used as the last
argument to the new
expression.
The above is equivalent to:
import i: interface {
f: func();
};
let my-instance = new example:my-component { i };
Note that implicit imports may not conflict with explicit imports of the same name.
Additionally, the WAC parser will attempt to merge implicit imports; if a merge cannot be performed, an error will be emitted about the conflicting definitions.
Let's assume there exists an example:my-other-component
with the following
world:
world my-other-component {
import i: interface {
g: func() -> string;
};
}
If we attempt to instantiate both example:my-component
and
example:my-other-component
using implicit imports:
let my-instance = new example:my-component { ... };
let my-other-instance = new example:my-other-component { ... };
Then the parser will merge the two implicit imports of i
together,
resulting in the following equivalent composition:
import i: interface {
f: func();
g: func() -> string;
};
let my-instance = new example:my-component { i };
let my-other-instance = new example:my-other-component { i };
As seen above, items that are implicitly imported are shared between any instantiations that refer to them.
The let
statement allows for binding a local name in the composition to the
result of an expression.
Note that local names are not variables and cannot be reassigned; a redefinition of a previous name is an error.
There are currently five expressions in WAC:
- A local name (e.g.
a
), resulting in the value of the name. - The
new
expression (e.gnew a:b { }
) - The postfix access expression (e.g.
a.b
). - The postfix named access expression (e.g.
a["b"]
) - The nested expression (e.g.
(<expr>)
).
A new
expression instantiates a component and returns a component instance
representing its exports.
The new
expression takes the name of the package (i.e. component) to
instantiate and a list of instantiation arguments (i.e. imports).
The last argument may be the special ...
argument that implies any missing
arguments should be imported from the composition and implicitly passed as
arguments to the instantiation.
If ...
is not specified, then all instantiation arguments must be explicitly
specified.
There are three forms of instantiation arguments: inferred arguments, named arguments, and spread arguments.
An inferred argument is passed directly by local name:
let i = new a:b { c };
Where i
is the name of the bound instance, a:b
is the package being
instantiated, and the local name c
is the instantiation argument.
The name of the instantiation argument is inferred according to these rules (in order of precedence):
-
If the local name is bound to an instance with an associated package path ( e.g.
foo:bar/baz
) and the component being instantiated has an import with a matching path, then the path will be used as the argument name. -
If the local name is bound to an explicit import or an access of an instance export and the component being instantiated has an import with a matching name, then the import/export name will be used as the argument name.
-
If the component being instantiated has exactly one import that has a path which ends with the local name (e.g.
foo:bar/c
), then the path will be used as the argument name. -
Lastly, the local name will be used as the argument name.
Named arguments provide the name of the instantiation argument to use:
let i = new a:b { c: d };
Where i
is the name of the bound instance, a:b
is the package being
instantiated, c
is the name of the instantiation argument, and d
is the
value of the argument.
The name may be specified as a kebab-case identifier (e.g. foo-bar
) or as a
string (e.g. "foo-bar"
).
When using a kebab-case identifier, a special rule is used to determine the actual name of the argument:
-
If the component being instantiated has exactly one import that has a path which ends with the local name (e.g.
foo:bar/foo-bar
), then the path will be used as the argument name. -
Otherwise, the kebab-case identifier will be used as the argument name.
With this rule, the following example is valid:
let stream = new my:stream {};
// Assumption: `a:b` has an import with path `wasi:io/input-stream`
let i = new a:b { input-stream: stream };
Spread arguments allow for specifying the exports of an instance as arguments to the instantiation of a component.
Like the ...
syntax for implicit imports, a ...
precedes a local name as an
indication to spread the instance's exports as arguments to the instantiation:
let i = new a:b { ...c };
Where i
is the name of the bound instance, a:b
is the package being
instantiated and c
is the local name of an instance.
It in an error for the local name used in a spread argument to name anything other than an instance.
With this syntax, the exports of the instance c
will be spread to any
unspecified and unsatisfied instantiation arguments.
Note that spread arguments apply after inferred and named arguments and are applied in-order; this means that the order of spread arguments is important:
let i = new a:b { foo: bar, ...c, baz, ...d };
In the above example, arguments foo
and baz
will bind to the instantiation
arguments of a:b
first.
Any unsatisfied arguments will then be satisfied by the matching exports of
instance c
, followed by any matching the exports of instance d
.
The above behavior differs from JavaScript's spread argument syntax, which is the inspiration for this syntax, because component instantiation arguments are named and not positional.
Additionally, it is an evaluation error if a spread argument has no matching exports.
The postfix access expression allows for accessing an export of an instance.
An example of instantiating component a:b
and then binding the wasi:io/outgoing-stream
export of the instance to s
.
let i = new a:b { ... };
let s = i.outgoing-stream;
Taking the above example into consideration, access expressions use the following rules to determine the name of the export:
-
If component
a:b
has exactly one export withoutgoing-stream
as the final component of a path (e.g.wasi:io/outgoing-stream
), then the path will be used as the export name. -
Otherwise,
outgoing-stream
will be used as the export name.
It is invalid to use an access expression on anything other than an instance.
Similar to the postfix access expression, the postfix named access expression is used to access an export of an instance.
However, this form allows for the export name to be explicitly specified as a string:
let i = new a:b { ... };
let s = i["wasi:io/outgoing-stream"];
This is equivalent to the previous example except that the export name is exactly what is specified by the string; no inference is performed.
The string may be any legal export name in the component model.
It is invalid to use a named access expression on anything other than an instance.
Export statements are used to export the result of an expression from the composition itself:
let my-run = new a:b { ... }.run;
export my-run;
// note: this is also equivalent to:
// export new a:b { ... }.run;
In the above example, component a:b
is instantiated and the local name
my-run
is bound to the export of run
from the instance, which we can assume
in this example to be a function of type func()
.
The export
statement is then used to export the my-run
function from the
composition using the export name run
as that is the name that was accessed
from the instance.
The as
keyword can be used to rename the export:
let my-run = new a:b { ... }.run;
export my-run as "my-run";
Like spread arguments in new
expressions, exports of an instance may be
spread as exports of the composition:
let my-instance = new a:b { ... };
export my-instance...;
If we assume that component a:b
in the above example has the following world:
interface setup {
setup: func(config: string);
}
world a:b {
export run: func();
export setup: setup;
}
Then the composition will export a run
function of type func()
and an
instance with name a:b/setup
.
It is an evaluation error if the instance being spread has no exports; it is a
syntax error to use an as
clause with a spread export.
Spread exports will only create new exports that do not conflict with previously exported items.
If we extend the above example to be:
let my-instance = new a:b { ... };
let run = b:c { ... }.run;
export run;
export my-instance...;
Then the run
function exported by the composition will be from the instance
named b:c
and not the instance named a:b
.
The current WAC grammar:
document ::= package-decl statement*
statement ::= import-statement
| type-statement
| let-statement
| export-statement
package-decl ::= `package` package-name (`targets` package-path)? `;`
package-name ::= id (':' id)+ ('@' version)?
version ::= <SEMVER>
import-statement ::= 'import' id ('as' (id | string))? ':' import-type ';'
import-type ::= package-path | func-type | inline-interface | id
package-path ::= id (':' id)+ ('/' id)+ ('@' version)?
type-statement ::= interface-decl | world-decl | type-decl
interface-decl ::= 'interface' id '{' interface-item* '}'
interface-item ::= use-type | item-type-decl | interface-export
use-type ::= 'use' use-path '.' '{' use-items '}' ';'
use-path ::= package-path | id
use-items ::= use-item (',' use-item)* ','?
use-item ::= id ('as' id)?
interface-export ::= id ':' func-type-ref ';'
world-decl ::= 'world' id '{' world-item* '}'
world-item ::= use-type
| item-type-decl
| world-import
| world-export
| world-include
world-import ::= 'import' world-item-path ';'
world-export ::= 'export' world-item-path ';'
world-item-path ::= named-world-item | package-path | id
named-world-item ::= id ':' extern-type
extern-type ::= func-type | inline-interface | id
inline-interface ::= 'interface' '{' interface-item* '}'
world-include ::= 'include' world-ref ('with' '{' world-include-items '}')? ';'
world-include-items ::= world-include-item (',' world-include-item)* ','?
world-include-item ::= id 'as' id
world-ref ::= package-path | id
type-decl ::= variant-decl | record-decl | flags-decl | enum-decl | type-alias
item-type-decl ::= resource-decl | type-decl
resource-decl ::= 'resource' id (';' | '{' resource-item* '}')
resource-item ::= constructor | method
constructor ::= 'constructor' param-list ';'
method ::= id ':' 'static'? func-type ';'
variant-decl ::= 'variant' id '{' variant-cases '}'
variant-cases ::= variant-case (',' variant-case)* ','?
variant-case ::= id ('(' type ')')?
record-decl ::= 'record' id '{' fields '}'
fields ::= named-type (',' named-type)* ','?
flags-decl ::= 'flags' id '{' ids '}'
ids ::= id (',' id)* ','?
enum-decl ::= 'enum' id '{' ids '}'
type-alias ::= 'type' id '=' (func-type | type) ';'
func-type-ref ::= func-type | id
func-type ::= 'func' '(' params? ')' ('->' results)?
params ::= named-type (',' named-type)* ','?
results ::= type
| '(' named-type (',' named-type)* ','? ')'
named-type ::= id ':' type
type ::= u8
| s8
| u16
| s16
| u32
| s32
| u64
| s64
| f32
| f64
| char
| bool
| string
| tuple
| list
| option
| result
| borrow
| id
tuple ::= 'tuple' '<' type (',' type)* ','? '>'
list ::= 'list' '<' type '>'
option ::= 'option' '<' type '>'
result ::= 'result'
| 'result' '<' type '>'
| 'result' '<' '_' ',' type '>'
| 'result' '<' type ',' type '>'
borrow ::= 'borrow' '<' type '>'
let-statement ::= 'let' id '=' expr ';'
expr ::= primary-expr postfix-expr*
primary-expr ::= new-expr | nested-expr | id
new-expr ::= 'new' package-name '{' instantiation-args '}'
instantiation-args ::= instantiation-arg (',' instantiation-arg)* (',' '...'?)?
instantiation-arg ::= id | '...' id | named-instantiation-arg
named-instantiation-arg ::= (id | string) ':' expr
nested-expr ::= '(' expr ')'
postfix-expr ::= access-expr | named-access-expr
access-expr ::= '.' id
named-access-expr ::= '[' string ']'
export-statement ::= 'export' expr (export-options)? ';'
export-options ::= `...` | 'as' (id | string)
id ::= '%'?[a-z][a-z0-9]*('-'[a-z][a-z0-9]*)*
string ::= '"' character-that-is-not-a-double-quote* '"'
Whitespace (may appear anywhere between tokens):
whitespace ::= ' ' | '\n' | '\r' | '\t' | comment
comment ::= '//' character-that-is-not-a-newline*
| '/*' any-unicode-character* '*/'
Note: block comments are allowed to be nested.