Skip to content

Commit

Permalink
feat: support top-level resources (#525)
Browse files Browse the repository at this point in the history
  • Loading branch information
guybedford authored Nov 21, 2024
1 parent 9e784e2 commit dc01f95
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 32 deletions.
12 changes: 8 additions & 4 deletions crates/js-component-bindgen/src/esm_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,14 @@ impl EsmBindgen {
}
iface = match iface.get_mut(&path[i]).unwrap() {
ImportBinding::Interface(iface) => iface,
ImportBinding::Local(_) => panic!(
"Imported interface {} cannot be both a function and an interface",
&path[0..i].join(".")
),
ImportBinding::Local(local) => {
panic!(
"Internal bindgen error: Import '{}' cannot be both an interface '{}' and a function '{}'",
&path[0..i + 1].join("."),
&path[i + 1..].join("."),
&local[0],
);
}
};
}
if let Some(ref mut existing) = iface.get_mut(&path[path.len() - 1]) {
Expand Down
51 changes: 41 additions & 10 deletions crates/js-component-bindgen/src/transpile_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1076,9 +1076,6 @@ impl<'a> Instantiator<'a, '_> {
let (import_name, _) = &self.component.import_types[*import_index];
let world_key = &self.imports[import_name];

// nested interfaces only currently possible through mapping
let (import_specifier, maybe_iface_member) = map_import(&self.gen.opts.map, import_name);

let (func, func_name, iface_name) =
match &self.resolve.worlds[self.world].imports[world_key] {
WorldItem::Function(func) => {
Expand All @@ -1098,6 +1095,27 @@ impl<'a> Instantiator<'a, '_> {
WorldItem::Type(_) => unreachable!(),
};

let resource_prefix = if iface_name.is_some() {
None
} else {
match func.kind {
FunctionKind::Method(_) => Some("[method]"),
FunctionKind::Static(_) => Some("[static]"),
FunctionKind::Constructor(_) => Some("[constructor]"),
FunctionKind::Freestanding => None,
}
};

// nested interfaces only currently possible through mapping
let (import_specifier, maybe_iface_member) = map_import(
&self.gen.opts.map,
if resource_prefix.is_some() {
import_name.strip_prefix(resource_prefix.unwrap()).unwrap()
} else {
import_name
},
);

let mut resource_map = ResourceMap::new();
self.create_resource_fn_map(func, func_ty, &mut resource_map);

Expand Down Expand Up @@ -1327,6 +1345,10 @@ impl<'a> Instantiator<'a, '_> {
self.gen
.esm_bindgen
.add_import_binding(&[import_specifier, import_binding], local_name);
} else if let Some(iface_member) = iface_member {
self.gen
.esm_bindgen
.add_import_binding(&[import_specifier, iface_member.into()], local_name);
} else {
self.gen
.esm_bindgen
Expand Down Expand Up @@ -1385,19 +1407,24 @@ impl<'a> Instantiator<'a, '_> {
wit_parser::TypeOwner::Interface(iface) => {
match &self.resolve.interfaces[iface].name {
Some(name) => (WorldKey::Interface(iface), Some(name.as_str())),
None => (
self.resolve.worlds[self.world]
None => {
let key = self.resolve.worlds[self.world]
.imports
.iter()
.find(|&(_, item)| match item {
WorldItem::Interface { id, .. } => *id == iface,
_ => false,
})
.unwrap()
.0
.clone(),
None,
),
.0;
(
key.clone(),
match key {
WorldKey::Name(ref name) => Some(name.as_str()),
WorldKey::Interface(_) => None,
},
)
}
}
}
wit_parser::TypeOwner::None => unimplemented!(),
Expand All @@ -1416,7 +1443,11 @@ impl<'a> Instantiator<'a, '_> {
import_specifier,
iface_name,
maybe_iface_member.as_deref(),
Some(resource_name),
if iface_name.is_some() {
Some(resource_name)
} else {
None
},
local_name_str.to_string(),
);

Expand Down
2 changes: 1 addition & 1 deletion crates/js-component-bindgen/src/ts_bindgen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ pub fn ts_bindgen(
TypeDefKind::Future(_) => todo!("generate for future"),
TypeDefKind::Stream(_) => todo!("generate for stream"),
TypeDefKind::Unknown => unreachable!(),
TypeDefKind::Resource => todo!(),
TypeDefKind::Resource => {}
TypeDefKind::Handle(_) => todo!(),
}
let output = gen.finish();
Expand Down
30 changes: 13 additions & 17 deletions test/codegen.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,23 @@ export async function readFlags (fixture) {
export async function codegenTest (fixtures) {
suite(`Transpiler codegen`, () => {
for (const fixture of fixtures) {
const name = fixture.replace(/(\.component)?\.(wasm|wat)$/, '');
const testName = fixture.replace(/(\.component)?\.(wasm|wat)$/, '');

for (const testFile of (readdirSync('test/runtime/')).filter(testFile => testFile.startsWith(`${name}.`))) {
const testName= testFile.replace(/\.ts$/, '');
test(`${testName} transpile`, async () => {
const flags = await readFlags(`test/runtime/${testName}.ts`);
var { stderr } = await exec(jcoPath, 'transpile', `test/fixtures/components/${fixture}`, '--name', testName, ...flags, '-o', `test/output/${testName}`);
strictEqual(stderr, '');
});

test(`${testName} transpile`, async () => {
const flags = await readFlags(`test/runtime/${testFile}`);
var { stderr } = await exec(jcoPath, 'transpile', `test/fixtures/components/${fixture}`, '--name', testName, ...flags, '-o', `test/output/${testName}`);
strictEqual(stderr, '');
});
test(`${testName} lint`, async () => {
const flags = await readFlags(`test/runtime/${testName}.ts`);

test(`${testName} lint`, async () => {
const flags = await readFlags(`test/runtime/${testFile}`);
if (flags.includes('--js'))
return;

if (flags.includes('--js'))
return;

var { stderr } = await exec(eslintPath, `test/output/${testName}/${testName}.js`, '-c', 'test/eslintrc.cjs');
strictEqual(stderr, '');
});
}
var { stderr } = await exec(eslintPath, `test/output/${testName}/${testName}.js`, '-c', 'test/eslintrc.cjs');
strictEqual(stderr, '');
});
}
});

Expand Down
Binary file not shown.
57 changes: 57 additions & 0 deletions test/runtime/resource_borrow_simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Flags: --map testwasi=../helpers.js --map r=../resource_borrow_simple.js#R test=../resource_borrow_simple.js#test

// @ts-nocheck
import * as assert from "assert";

/*
WIT definition (for reference):
package root:component;
world root {
import wasi:cli/[email protected];
import wasi:cli/[email protected];
import wasi:io/[email protected];
import wasi:io/[email protected];
import wasi:cli/[email protected];
import wasi:cli/[email protected];
import wasi:cli/[email protected];
import wasi:clocks/[email protected];
import wasi:filesystem/[email protected];
import wasi:filesystem/[email protected];
import test: func(r: borrow<r>);
resource r {
constructor();
}
export test-imports: func();
}
*/

// Imports
let constructed = false;
export class R {
constructor() {
constructed = true;
}
}

let tested = false;
export function test(r) {
assert.ok(r instanceof R);
tested = true;
}

export async function run() {
const wasm = await import(
"../output/resource_borrow_simple/resource_borrow_simple.js"
);

wasm.testImports();
assert.ok(constructed);
assert.ok(tested);
}

// TLA cycle avoidance
setTimeout(run);
6 changes: 6 additions & 0 deletions test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"include": ["runtime/*.ts"],
"TODO": "why are these runtime tests excluded?",
"exclude": [
"runtime/strings.async+js.ts",
"runtime/strings.sync+js.ts",
"runtime/strings.sync.ts"
],
"compilerOptions": {
"outDir": "output",
"module": "nodenext",
Expand Down

0 comments on commit dc01f95

Please sign in to comment.