mod codec;
mod host;
mod names;
mod types;
use std::collections::{HashMap, HashSet, VecDeque};
use std::fmt;
use std::path::Path;
use hyperlight_component_util::emit::{State, split_wit_name};
use hyperlight_component_util::etypes::{Component, ExternDesc, Value};
use hyperlight_component_util::util::{WitSource, read_wit_type};
use names::{Src, pascal};
pub const RUNTIME_SOURCE: &str = include_str!("Runtime.cs");
const RT: &str = "global::Hyperlight.Component";
#[derive(Debug)]
pub struct Error(String);
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::error::Error for Error {}
fn error(msg: impl Into<String>) -> Error {
Error(msg.into())
}
pub fn generate_from_path(
path: &Path,
world: Option<&str>,
namespace: &str,
) -> Result<String, Error> {
generate(WitSource::Wit(path.to_path_buf()), world, namespace)
}
pub fn generate(source: WitSource, world: Option<&str>, namespace: &str) -> Result<String, Error> {
if !namespace.split('.').all(names::is_identifier) {
return Err(error(format!("invalid C# namespace '{namespace}'")));
}
let world = world.map(str::to_string);
read_wit_type(source, world, |name, ct| emit(&name, ct, namespace))
}
type S<'a, 'b> = State<'a, 'b, Root<'b>>;
fn emit(name: &str, ct: &Component<'_>, namespace: &str) -> Result<String, Error> {
let mut root = Root::new(namespace, name, ct);
let mut bound_vars = ct
.instance
.evars
.iter()
.rev()
.chain(ct.uvars.iter().rev())
.cloned()
.collect::<VecDeque<_>>();
let mut vars_needs_vars = VecDeque::new();
let mut s = State::new(
&mut root,
&mut bound_vars,
&mut vars_needs_vars,
false,
false,
);
types::emit_toplevel(&mut s, ct)?;
host::emit_toplevel(&mut s, ct)?;
Ok(root.render())
}
struct Decl<'b> {
var: usize,
aliases: Vec<usize>,
value: Value<'b>,
namespace: String,
name: String,
mangled: String,
world_level: bool,
instance: Option<String>,
}
struct ResourceDecl {
var: usize,
wit_name: String,
instance: Option<String>,
namespace: String,
name: String,
table: String,
}
pub(crate) struct Root<'b> {
ns: String,
world: String,
world_type: String,
codec: String,
resources_class: String,
decls: Vec<Decl<'b>>,
resources: Vec<ResourceDecl>,
versioned: HashSet<String>,
world_package: (Vec<String>, String),
temps: usize,
namespaces: Vec<(String, Src)>,
codec_methods: Src,
}
impl<'b> Root<'b> {
fn new(ns: &str, world: &str, ct: &Component<'_>) -> Self {
let wn = split_wit_name(world);
let world_type = pascal(wn.name);
let world_package = (
wn.namespaces
.iter()
.map(|n| n.to_string())
.collect::<Vec<_>>(),
wn._version.join("@"),
);
let mut versions = HashMap::<String, HashSet<String>>::new();
for ed in ct.imports.iter().chain(&ct.instance.unqualified.exports) {
if let ExternDesc::Instance(_) = ed.desc {
let (namespaces, version) = package(&world_package, ed.kebab_name);
if !namespaces.is_empty() {
versions
.entry(namespaces.join(":"))
.or_default()
.insert(version);
}
}
}
Root {
ns: ns.to_string(),
world: world.to_string(),
codec: format!("global::{ns}.{world_type}Codec"),
resources_class: format!("global::{ns}.{world_type}Resources"),
world_type,
decls: Vec::new(),
resources: Vec::new(),
versioned: versions
.into_iter()
.filter(|(_, v)| v.len() > 1)
.map(|(k, _)| k)
.collect(),
world_package,
temps: 0,
namespaces: Vec::new(),
codec_methods: Src::default(),
}
}
fn add(&mut self, ns: String, src: Src) {
match self.namespaces.iter_mut().find(|(n, _)| *n == ns) {
Some((_, existing)) => {
existing.line("");
existing.append(src);
}
None => self.namespaces.push((ns, src)),
}
}
fn add_codec_methods(&mut self, src: Src) {
if !self.codec_methods.is_empty() {
self.codec_methods.line("");
}
self.codec_methods.append(src);
}
fn render(mut self) -> String {
let name = self.codec.rsplit('.').next().unwrap().to_string();
let mut codec = Src::default();
codec.line(format!("internal static class {name}"));
codec.open();
codec.append(std::mem::take(&mut self.codec_methods));
codec.close();
self.add(self.ns.clone(), codec);
let mut out = Src::default();
out.line("// <auto-generated>");
out.line(format!(
"// Generated by hyperlight-component-csharp from WIT world {}.",
self.world
));
out.line("// </auto-generated>");
out.line("");
out.line("#nullable enable");
for (ns, src) in self.namespaces {
out.line("");
out.line(format!("namespace {ns}"));
out.open();
out.append(src);
out.close();
}
out.into_string()
}
fn package_namespace(&self, instance: &str) -> Option<String> {
let (namespaces, version) = package(&self.world_package, instance);
if namespaces.is_empty() {
return None;
}
let mut ns = namespaces
.iter()
.map(|n| pascal(n))
.collect::<Vec<_>>()
.join(".");
if self.versioned.contains(&namespaces.join(":")) {
let version = version
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect::<String>();
ns.push_str(&format!(".V{version}"));
}
Some(ns)
}
fn interface_namespace(&self, instance: &str) -> String {
let name = pascal(split_wit_name(instance).name);
match self.package_namespace(instance) {
Some(pkg) => format!("{pkg}.{name}"),
None => name,
}
}
fn qualify(&self, relative: Option<String>) -> String {
match relative {
Some(ns) => format!("{}.{ns}", self.ns),
None => self.ns.clone(),
}
}
fn temp(&mut self) -> String {
self.temps += 1;
format!("__t{}", self.temps - 1)
}
fn decl_index(&self, var: usize) -> Option<usize> {
self.decls
.iter()
.position(|d| d.var == var || d.aliases.contains(&var))
}
fn type_name(&self, d: &Decl) -> String {
format!("global::{}.{}", d.namespace, d.name)
}
fn resource_index(&self, var: usize) -> Option<usize> {
self.resources.iter().position(|r| r.var == var)
}
fn resource_table(&self, i: usize, resources: &str) -> String {
format!(
"(({}){resources}!).{}",
self.resources_class, self.resources[i].table
)
}
}
fn package(world_package: &(Vec<String>, String), instance: &str) -> (Vec<String>, String) {
let wn = split_wit_name(instance);
if wn.namespaces.is_empty() {
return world_package.clone();
}
(
wn.namespaces.iter().map(|n| n.to_string()).collect(),
wn._version.join("@"),
)
}
#[cfg(test)]
mod tests;