diff --git a/rust/candid/src/parser/grammar.lalrpop b/rust/candid/src/parser/grammar.lalrpop index 850079c7..6f0e2356 100644 --- a/rust/candid/src/parser/grammar.lalrpop +++ b/rust/candid/src/parser/grammar.lalrpop @@ -1,7 +1,7 @@ use crate::types::value::{IDLField, IDLValue, IDLArgs, VariantValue}; use crate::types::{TypeEnv, FuncMode}; use crate::utils::check_unique; -use super::types::{IDLType, PrimType, TypeField, FuncType, Binding, Dec, IDLProg, IDLTypes}; +use super::types::{IDLType, PrimType, TypeField, FuncType, Binding, Dec, IDLProg, IDLTypes, IDLInitArgs}; use super::test::{Assert, Input, Test}; use super::token::{Token, error2, LexicalError, Span}; use crate::{Principal, types::Label}; @@ -272,6 +272,10 @@ pub IDLProg: IDLProg = { > => IDLProg { decs, actor } } +pub IDLInitArgs: IDLInitArgs = { + > => IDLInitArgs { decs, args } +} + // Test file Input: Input = { diff --git a/rust/candid/src/parser/types.rs b/rust/candid/src/parser/types.rs index 738c719e..5c9794f6 100644 --- a/rust/candid/src/parser/types.rs +++ b/rust/candid/src/parser/types.rs @@ -91,6 +91,12 @@ pub struct IDLProg { pub actor: Option, } +#[derive(Debug)] +pub struct IDLInitArgs { + pub decs: Vec, + pub args: Vec, +} + impl std::str::FromStr for IDLProg { type Err = crate::Error; fn from_str(str: &str) -> Result { @@ -98,6 +104,13 @@ impl std::str::FromStr for IDLProg { Ok(super::grammar::IDLProgParser::new().parse(lexer)?) } } +impl std::str::FromStr for IDLInitArgs { + type Err = crate::Error; + fn from_str(str: &str) -> Result { + let lexer = super::token::Tokenizer::new(str); + Ok(super::grammar::IDLInitArgsParser::new().parse(lexer)?) + } +} impl std::str::FromStr for IDLType { type Err = crate::Error; diff --git a/rust/candid/src/parser/typing.rs b/rust/candid/src/parser/typing.rs index 2d6f0983..1b126d59 100644 --- a/rust/candid/src/parser/typing.rs +++ b/rust/candid/src/parser/typing.rs @@ -241,12 +241,28 @@ fn load_imports( } /// Type check IDLProg and adds bindings to type environment. Returns -/// a hash map for the serivce method signatures. This function ignores the imports. +/// the main actor if present. This function ignores the imports. pub fn check_prog(te: &mut TypeEnv, prog: &IDLProg) -> Result> { let mut env = Env { te, pre: false }; check_decs(&mut env, &prog.decs)?; check_actor(&env, &prog.actor) } +/// Type check init args extracted from canister metadata candid:args. +/// Need to provide `main_env`, because init args may refer to variables from the main did file. +pub fn check_init_args( + te: &mut TypeEnv, + main_env: &TypeEnv, + prog: &IDLInitArgs, +) -> Result> { + let mut env = Env { te, pre: false }; + check_decs(&mut env, &prog.decs)?; + env.te.merge(main_env)?; + let mut args = Vec::new(); + for arg in prog.args.iter() { + args.push(check_type(&env, arg)?); + } + Ok(args) +} fn check_file_(file: &Path, is_pretty: bool) -> Result<(TypeEnv, Option)> { let base = if file.is_absolute() { diff --git a/rust/candid/src/types/internal.rs b/rust/candid/src/types/internal.rs index 9cfa5a16..b93f407a 100644 --- a/rust/candid/src/types/internal.rs +++ b/rust/candid/src/types/internal.rs @@ -356,9 +356,9 @@ impl fmt::Display for Field { /// Construct a field type, which can be used in `TypeInner::Record` and `TypeInner::Variant`. /// /// `field!{ a: TypeInner::Nat.into() }` expands to `Field { id: Label::Named("a"), ty: ... }` -/// `field!{ 0: TypeInner::Nat.into() }` expands to `Field { id: Label::Id(0), ty: ... }` +/// `field!{ 0: Nat::ty() }` expands to `Field { id: Label::Id(0), ty: ... }` macro_rules! field { - { $id:tt : $ty:expr } => { + { $id:tt : $ty:expr } => {{ $crate::types::internal::Field { id: match stringify!($id).parse::() { Ok(id) => $crate::types::Label::Id(id), @@ -366,7 +366,31 @@ macro_rules! field { }.into(), ty: $ty } - }; + }} +} +#[macro_export] +/// Construct a record type, e.g., `record!{ label: Nat::ty(); 42: String::ty() }`. +macro_rules! record { + { $($id:tt : $ty:expr);* $(;)? } => {{ + let mut fs: Vec<$crate::types::internal::Field> = vec![ $($crate::field!{$id : $ty}),* ]; + fs.sort_unstable_by_key(|f| f.id.get_id()); + if let Err(e) = $crate::utils::check_unique(fs.iter().map(|f| &f.id)) { + panic!("{e}"); + } + Into::<$crate::types::Type>::into($crate::types::TypeInner::Record(fs)) + }} +} +#[macro_export] +/// Construct a variant type, e.g., `variant!{ tag: <()>::ty() }`. +macro_rules! variant { + { $($id:tt : $ty:expr);* $(;)? } => {{ + let mut fs: Vec<$crate::types::internal::Field> = vec![ $($crate::field!{$id : $ty}),* ]; + fs.sort_unstable_by_key(|f| f.id.get_id()); + if let Err(e) = $crate::utils::check_unique(fs.iter().map(|f| &f.id)) { + panic!("{e}"); + } + Into::<$crate::types::Type>::into($crate::types::TypeInner::Variant(fs)) + }} } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -431,7 +455,7 @@ macro_rules! func { /// `service!{ "f": func!((HttpRequest) -> ()) }` expands to `Type(Rc::new(TypeInner::Service(...)))` macro_rules! service { { $($meth:tt : $ty:expr);* $(;)? } => {{ - let mut ms = vec![ $(($meth.to_string(), $ty)),* ]; + let mut ms: Vec<(String, $crate::types::Type)> = vec![ $(($meth.to_string(), $ty)),* ]; ms.sort_unstable_by(|a, b| a.0.as_str().partial_cmp(b.0.as_str()).unwrap()); if let Err(e) = $crate::utils::check_unique(ms.iter().map(|m| &m.0)) { panic!("{e}"); diff --git a/rust/candid/src/utils.rs b/rust/candid/src/utils.rs index c8f275e3..c251be65 100644 --- a/rust/candid/src/utils.rs +++ b/rust/candid/src/utils.rs @@ -82,7 +82,7 @@ pub fn service_equal(left: CandidSource, right: CandidSource) -> Result<()> { #[cfg_attr(docsrs, doc(cfg(feature = "parser")))] #[cfg(feature = "parser")] /// Take a did file and outputs the init args and the service type (without init args). -/// If the original did file contains imports, the output flatens the type definitions. +/// If the original did file contains imports, the output flattens the type definitions. /// For now, the comments from the original did file is omitted. pub fn instantiate_candid(candid: CandidSource) -> Result<(Vec, (TypeEnv, Type))> { use crate::types::TypeInner; @@ -95,6 +95,28 @@ pub fn instantiate_candid(candid: CandidSource) -> Result<(Vec, (TypeEnv, }) } +/// Merge canister metadata candid:args and candid:service into a service constructor. +/// If candid:service already contains init args, returns the original did file. +#[cfg_attr(docsrs, doc(cfg(feature = "parser")))] +#[cfg(feature = "parser")] +pub fn merge_init_args(candid: &str, init: &str) -> Result<(TypeEnv, Type)> { + use crate::parser::{types::IDLInitArgs, typing::check_init_args}; + use crate::types::TypeInner; + let candid = CandidSource::Text(candid); + let (env, serv) = candid.load()?; + let serv = serv.ok_or_else(|| Error::msg("the Candid interface has no main service type"))?; + match serv.as_ref() { + TypeInner::Class(_, _) => Ok((env, serv)), + TypeInner::Service(_) => { + let prog = init.parse::()?; + let mut env2 = TypeEnv::new(); + let args = check_init_args(&mut env2, &env, &prog)?; + Ok((env2, TypeInner::Class(args, serv).into())) + } + _ => unreachable!(), + } +} + /// Encode sequence of Rust values into Candid message of type `candid::Result>`. #[macro_export] macro_rules! Encode { diff --git a/rust/candid/tests/parse_value.rs b/rust/candid/tests/parse_value.rs index 57710bf5..027401db 100644 --- a/rust/candid/tests/parse_value.rs +++ b/rust/candid/tests/parse_value.rs @@ -1,5 +1,6 @@ use candid::types::value::{IDLArgs, IDLField, IDLValue, VariantValue}; use candid::types::{Label, Type, TypeEnv, TypeInner}; +use candid::{record, variant, CandidType, Nat}; fn parse_args(input: &str) -> IDLArgs { input.parse().unwrap() @@ -136,6 +137,12 @@ fn parse_optional_record() { let mut args = parse_args("(opt record {}, record { 1=42;44=\"test\"; 2=false }, variant { 5=null })"); let typ = parse_type("record { 1: nat; 44: text; 2: bool }"); + assert_eq!( + typ, + record! { 1: Nat::ty(); 44: String::ty(); 2: bool::ty() } + ); + assert_eq!(args.args[0].value_ty(), TypeInner::Opt(record! {}).into()); + assert_eq!(args.args[2].value_ty(), variant! { 5: <()>::ty() }); args.args[1] = args.args[1] .annotate_type(true, &TypeEnv::new(), &typ) .unwrap(); @@ -180,6 +187,10 @@ fn parse_nested_record() { let typ = parse_type( "record {label: nat; 0x2b:record { test:text; \"opt\":text }; long_label: opt null }", ); + assert_eq!( + typ, + record! {label: Nat::ty(); 43: record!{ test: String::ty(); opt: String::ty() }; long_label: Option::<()>::ty(); } + ); args.args[0] = args.args[0] .annotate_type(true, &TypeEnv::new(), &typ) .unwrap(); diff --git a/tools/ui/Cargo.lock b/tools/ui/Cargo.lock index 014b3103..47229acc 100644 --- a/tools/ui/Cargo.lock +++ b/tools/ui/Cargo.lock @@ -111,15 +111,14 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "candid" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "762aa04e3a889d47a1773b74ee3b13438a9bc895954fff79ebf7f308c3744a6c" +version = "0.9.6" dependencies = [ "anyhow", "binread", "byteorder", "candid_derive", "codespan-reporting", + "convert_case", "crc32fast", "data-encoding", "hex", @@ -141,14 +140,12 @@ dependencies = [ [[package]] name = "candid_derive" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "810b3bd60244f282090652ffc7c30a9d23892e72dfe443e46ee55569044f7dd5" +version = "0.6.3" dependencies = [ "lazy_static", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -176,6 +173,15 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cpufeatures" version = "0.2.9" @@ -511,7 +517,7 @@ dependencies = [ "proc-macro2", "quote", "regex-syntax 0.6.29", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -525,9 +531,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.6.2" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "new_debug_unreachable" @@ -584,7 +590,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -732,9 +738,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.4" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12de2eff854e5fa4b1295edd650e227e9d8fb0c9e90b12e7f36d6a6811791a29" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", @@ -744,9 +750,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.7" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49530408a136e16e5b486e883fbb6ba058e8e4e8ae6621a77b048b314336e629" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", @@ -767,9 +773,9 @@ checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "rustix" -version = "0.38.10" +version = "0.38.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed6248e1caa625eb708e266e06159f135e8c26f2bb7ceb72dc4b2766d0340964" +checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453" dependencies = [ "bitflags 2.4.0", "errno", @@ -816,7 +822,7 @@ checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -892,9 +898,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" dependencies = [ "proc-macro2", "quote", @@ -923,22 +929,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.31", ] [[package]] @@ -958,9 +964,9 @@ checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap", "toml_datetime", diff --git a/tools/ui/Cargo.toml b/tools/ui/Cargo.toml index 4c9d1a40..4eb9f146 100644 --- a/tools/ui/Cargo.toml +++ b/tools/ui/Cargo.toml @@ -5,4 +5,8 @@ members = [ [profile.release] lto = true -opt-level = 'z' +opt-level = 2 + +[patch.crates-io.candid] +path = "../../rust/candid" + diff --git a/tools/ui/src/didjs/didjs.did b/tools/ui/src/didjs/didjs.did index 4b2c8cad..092cfa44 100644 --- a/tools/ui/src/didjs/didjs.did +++ b/tools/ui/src/didjs/didjs.did @@ -2,4 +2,5 @@ service : { did_to_js : (text) -> (opt text) query; binding : (text, text) -> (opt text) query; subtype : (new: text, old: text) -> (variant { Ok: null; Err: text }) query; + merge_init_args : (text, text) -> (opt text) query } diff --git a/tools/ui/src/didjs/lib.rs b/tools/ui/src/didjs/lib.rs index 99f9e2b9..62c5d657 100644 --- a/tools/ui/src/didjs/lib.rs +++ b/tools/ui/src/didjs/lib.rs @@ -58,6 +58,13 @@ fn binding(prog: String, lang: String) -> Option { Some(res) } +#[ic_cdk::query] +fn merge_init_args(prog: String, init_args: String) -> Option { + use candid::bindings; + let (env, actor) = candid::utils::merge_init_args(&prog, &init_args).ok()?; + Some(bindings::candid::compile(&env, &Some(actor))) +} + #[ic_cdk::query] fn subtype(new: String, old: String) -> Result<(), String> { let new = new.parse::().unwrap();