Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@
my_variant_field : nat;
};
```
+ Adds the `IDLMergedProg` struct, used to collect the syntax types when parsing Candid declarations.
+ Supports reflecting doc comments from Candid declarations to the Motoko generated bindings. The `candid_parser::bindings::motoko::compile` function now takes a `&IDLMergedProg` parameter.

### candid_derive

Expand Down
123 changes: 121 additions & 2 deletions rust/candid/src/types/syntax.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
use crate::types::{FuncMode, Label};
use crate::{
idl_hash,
types::{FuncMode, Label},
};
use anyhow::{anyhow, bail, Context, Result};
use std::collections::HashMap;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IDLType {
Expand Down Expand Up @@ -111,7 +116,7 @@ pub struct Binding {
pub docs: Vec<String>,
}

#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct IDLActorType {
pub typ: IDLType,
pub docs: Vec<String>,
Expand All @@ -123,8 +128,122 @@ pub struct IDLProg {
pub actor: Option<IDLActorType>,
}

impl IDLProg {
pub fn typ_decs(decs: Vec<Dec>) -> impl Iterator<Item = Binding> {
decs.into_iter().filter_map(|d| {
if let Dec::TypD(bindings) = d {
Some(bindings)
} else {
None
}
})
}
}

#[derive(Debug)]
pub struct IDLInitArgs {
pub decs: Vec<Dec>,
pub args: Vec<IDLArgType>,
}

#[derive(Debug)]
pub struct IDLMergedProg {
typ_decs: Vec<Binding>,
main_actor: Option<IDLActorType>,
service_imports: Vec<(String, IDLActorType)>,
}

impl IDLMergedProg {
pub fn new(prog: IDLProg) -> IDLMergedProg {
IDLMergedProg {
typ_decs: IDLProg::typ_decs(prog.decs).collect(),
main_actor: prog.actor,
service_imports: vec![],
}
}

pub fn merge(&mut self, is_service_import: bool, name: String, prog: IDLProg) -> Result<()> {
self.typ_decs.extend(IDLProg::typ_decs(prog.decs));
if is_service_import {
let actor = prog
.actor
.with_context(|| format!("Imported service file \"{name}\" has no main service"))?;
self.service_imports.push((name, actor));
}
Ok(())
}

pub fn lookup(&self, id: &str) -> Option<&Binding> {
self.typ_decs.iter().find(|b| b.id == id)
}

pub fn decs(&self) -> Vec<Dec> {
self.typ_decs.iter().map(|b| Dec::TypD(b.clone())).collect()
}

pub fn resolve_actor(&self) -> Result<Option<IDLActorType>> {
let (init_args, top_level_docs, mut methods) = match &self.main_actor {
None => {
if self.service_imports.is_empty() {
return Ok(None);
} else {
(None, vec![], vec![])
}
}
Some(t) if self.service_imports.is_empty() => return Ok(Some(t.clone())),
Some(IDLActorType {
typ: IDLType::ClassT(args, inner),
docs,
}) => (
Some(args.clone()),
docs.clone(),
self.chase_service(*inner.clone(), None)?,
),
Some(ty) => (
None,
ty.docs.clone(),
self.chase_service(ty.typ.clone(), None)?,
),
};

for (name, typ) in &self.service_imports {
methods.extend(self.chase_service(typ.typ.clone(), Some(name))?);
}

let mut hashes: HashMap<u32, &str> = HashMap::new();
for method in &methods {
let name = &method.id;
if let Some(previous) = hashes.insert(idl_hash(name), name) {
bail!("Duplicate imported method name: label '{name}' hash collision with '{previous}'")
}
}

let typ = if let Some(args) = init_args {
IDLType::ClassT(args, Box::new(IDLType::ServT(methods)))
} else {
IDLType::ServT(methods)
};
Ok(Some(IDLActorType {
typ,
docs: top_level_docs,
}))
}

// NOTE: We don't worry about cyclic type definitions, as we rule those out earlier when checking the type decs
fn chase_service(&self, ty: IDLType, import_name: Option<&str>) -> Result<Vec<Binding>> {
match ty {
IDLType::VarT(v) => {
let resolved = self
.typ_decs
.iter()
.find(|b| b.id == v)
.with_context(|| format!("Unbound type identifier {v}"))?;
self.chase_service(resolved.typ.clone(), import_name)
}
IDLType::ServT(bindings) => Ok(bindings),
ty => Err(import_name
.map(|name| anyhow!("Imported service file \"{name}\" has a service constructor"))
.unwrap_or(anyhow!("not a service type: {:?}", ty))),
}
}
}
Loading