From 733a806f7a376022a9b73b5837549789f131a0e5 Mon Sep 17 00:00:00 2001 From: Yan Date: Sun, 2 May 2021 15:18:00 -0700 Subject: [PATCH] Add Value module to support path and blob inside Candid value (#4) * add Value module to support path and blob inside Candid value * easier than I thought :) * encode command * encode value * fix Co-authored-by: chenyan-dfinity --- README.md | 50 ++++++++++++++++----- src/command.rs | 81 ++++++++++++---------------------- src/grammar.lalrpop | 92 ++++++++++++++++++++++---------------- src/helper.rs | 3 +- src/main.rs | 1 + src/token.rs | 2 + src/value.rs | 105 ++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 229 insertions(+), 105 deletions(-) create mode 100644 src/value.rs diff --git a/README.md b/README.md index 10b36db..9786b8e 100644 --- a/README.md +++ b/README.md @@ -8,19 +8,26 @@ ic-repl --replica [local|ic|url] --config [script file] ``` := - | import = [ : ] (canister URI with optional did file) - | export (filename) - | load (filename) - | config (dhall config) - | call . ( ,* ) - | let = - | show - | assert - | identity + | import = [ : ] // bind canister URI to , with optional did file + | call . ( ,* ) // call a canister method with candid arguments + | encode . ( ,* ) // encode candid arguments with respect to a canister method signature + | export // export command history to a file that can be run in ic-repl as a script + | load // load and run a script file + | config // set config for random value generator in dhall format + | let = // bind to a variable + | show // show the value of + | assert // assertion + | identity // switch to identity (create a new one if doesn't exist) - := | _ - := | (. )* | file - := == | ~= | != + := | _ (previous call result is bind to `_`) + := + | | (. )* + | file // load external file as a blob value + | encode ( := + | == // structural equality + | ~= // equal under candid subtyping + | != // not equal ``` ## Example @@ -38,6 +45,23 @@ call "rrkah-fqaaa-aaaaa-aaaaq-cai".greet("test"); assert _ == result; ``` +install.sh +``` +#!/usr/bin/ic-repl -r ic +call "aaaaa-aa".provisional_create_canister_with_cycles(record { settings: null; amount: null }); +let id = _; +call "aaaaa-aa".install_code( + record { + arg = encode (); + wasm_module = file "your_wasm_file.wasm"; + mode = variant { install }; + canister_id = id.canister_id; // TODO + }, +); +call "aaaaa-aa".canister_status(id); +call id.canister_id.greet("test"); +``` + ## Notes for Rust canisters `ic-repl` relies on the `__get_candid_interface_tmp_hack` canister method to fetch the Candid interface. The default @@ -58,6 +82,8 @@ If you are writing your own `.did` file, you can also supply the did file via th ## Issues +* Acess to service init type +* `IDLValue::Blob` for efficient blob serialization * Autocompletion within Candid value * Robust support for `~=`, requires inferring principal types * Value projection diff --git a/src/command.rs b/src/command.rs index be3938c..ff3f3ef 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,6 +1,7 @@ use super::error::pretty_parse; use super::helper::{did_to_canister_info, MyHelper, NameEnv}; use super::token::{ParserError, Spanned, Tokenizer}; +use super::value::Value; use anyhow::{anyhow, Context}; use candid::{ parser::configs::Configs, parser::value::IDLValue, types::Function, IDLArgs, Principal, TypeEnv, @@ -11,44 +12,15 @@ use std::path::{Path, PathBuf}; use std::time::Instant; use terminal_size::{terminal_size, Width}; -#[derive(Debug, Clone)] -pub enum Value { - Candid(IDLValue), - Path(Vec), - Blob(String), -} -impl Value { - fn get<'a>(&'a self, helper: &'a MyHelper) -> anyhow::Result { - Ok(match self { - Value::Candid(v) => v.clone(), - Value::Path(vs) => helper - .env - .0 - .get(&vs[0]) - .ok_or_else(|| anyhow!("Undefined variable {}", vs[0]))? - .clone(), - Value::Blob(file) => { - let path = resolve_path(&helper.base_path, PathBuf::from(file)); - let blob: Vec = std::fs::read(&path) - .with_context(|| format!("Cannot read {:?}", path))? - .into_iter() - .map(IDLValue::Nat8) - .collect(); - IDLValue::Vec(blob) - } - }) - } -} - #[derive(Debug, Clone)] pub struct Commands(pub Vec); - #[derive(Debug, Clone)] pub enum Command { Call { canister: Spanned, method: String, args: Vec, + encode_only: bool, }, Config(String), Show(Value), @@ -67,12 +39,13 @@ pub enum BinOp { } impl Command { - pub fn run(&self, helper: &mut MyHelper) -> anyhow::Result<()> { + pub fn run(self, helper: &mut MyHelper) -> anyhow::Result<()> { match self { Command::Call { canister, method, args, + encode_only, } => { let try_id = Principal::from_text(&canister.value); let canister_id = match try_id { @@ -88,13 +61,20 @@ impl Command { let info = map.get(&agent, canister_id)?; let func = info .methods - .get(method) + .get(&method) .ok_or_else(|| anyhow!("no method {}", method))?; let mut values = Vec::new(); - for arg in args.iter() { - values.push(arg.get(&helper)?); + for arg in args.into_iter() { + values.push(arg.eval(&helper)?); } let args = IDLArgs { args: values }; + if encode_only { + let bytes = args.to_bytes_with_types(&info.env, &func.args)?; + let res = IDLValue::Vec(bytes.into_iter().map(IDLValue::Nat8).collect()); + println!("{}", res); + helper.env.0.insert("_".to_string(), res); + return Ok(()); + } let time = Instant::now(); let res = call(&agent, canister_id, &method, &args, &info.env, &func)?; let duration = time.elapsed(); @@ -111,29 +91,26 @@ impl Command { } } Command::Import(id, canister_id, did) => { - if let Some(did) = did { + if let Some(did) = &did { let path = resolve_path(&helper.base_path, PathBuf::from(did)); let src = std::fs::read_to_string(&path) .with_context(|| format!("Cannot read {:?}", path))?; - let info = did_to_canister_info(did, &src)?; + let info = did_to_canister_info(&did, &src)?; helper .canister_map .borrow_mut() .0 .insert(canister_id.clone(), info); } - helper - .canister_env - .0 - .insert(id.to_string(), canister_id.clone()); + helper.canister_env.0.insert(id, canister_id); } Command::Let(id, val) => { - let v = val.get(&helper)?; - helper.env.0.insert(id.to_string(), v); + let v = val.eval(&helper)?; + helper.env.0.insert(id, v); } Command::Assert(op, left, right) => { - let left = left.get(&helper)?; - let right = right.get(&helper)?; + let left = left.eval(&helper)?; + let right = right.eval(&helper)?; match op { BinOp::Equal => assert_eq!(left, right), BinOp::SubEqual => { @@ -153,12 +130,12 @@ impl Command { } Command::Config(conf) => helper.config = Configs::from_dhall(&conf)?, Command::Show(val) => { - let v = val.get(&helper)?; + let v = val.eval(&helper)?; println!("{}", v); } Command::Identity(id) => { use ic_agent::Identity; - let keypair = if let Some(keypair) = helper.identity_map.0.get(id) { + let keypair = if let Some(keypair) = helper.identity_map.0.get(&id) { keypair.to_vec() } else { let rng = ring::rand::SystemRandom::new(); @@ -191,10 +168,7 @@ impl Command { } helper.agent = agent; helper.current_identity = id.to_string(); - helper - .env - .0 - .insert(id.to_string(), IDLValue::Principal(sender)); + helper.env.0.insert(id, IDLValue::Principal(sender)); } Command::Export(file) => { use std::io::{BufWriter, Write}; @@ -207,7 +181,7 @@ impl Command { Command::Load(file) => { // TODO check for infinite loop let old_base = helper.base_path.clone(); - let path = resolve_path(&old_base, PathBuf::from(file)); + let path = resolve_path(&old_base, PathBuf::from(&file)); let mut script = std::fs::read_to_string(&path) .with_context(|| format!("Cannot read {:?}", path))?; if script.starts_with("#!") { @@ -216,7 +190,7 @@ impl Command { } let cmds = pretty_parse::(&file, &script)?; helper.base_path = path.parent().unwrap().to_path_buf(); - for cmd in cmds.0.iter() { + for cmd in cmds.0.into_iter() { println!("> {:?}", cmd); cmd.run(helper)?; } @@ -286,6 +260,7 @@ pub fn extract_canister( canister, method, args, + .. } => { let try_id = Principal::from_text(&canister.value); let canister_id = match try_id { @@ -298,7 +273,7 @@ pub fn extract_canister( } } -fn resolve_path(base: &Path, file: PathBuf) -> PathBuf { +pub fn resolve_path(base: &Path, file: PathBuf) -> PathBuf { if file.is_absolute() { file } else { diff --git a/src/grammar.lalrpop b/src/grammar.lalrpop index 43c9dc4..05ab470 100644 --- a/src/grammar.lalrpop +++ b/src/grammar.lalrpop @@ -1,9 +1,9 @@ -use candid::parser::value::{IDLField, IDLValue, IDLArgs, VariantValue}; +use super::value::{Field, Value}; use candid::parser::types::{IDLType, TypeField, PrimType, FuncType, FuncMode, Binding}; use candid::parser::typing::{TypeEnv, check_unique}; use super::token::{Token, error2, LexicalError, Span, Spanned}; use candid::{Principal, types::Label}; -use super::command::{Command, Commands, Value, BinOp}; +use super::command::{Command, Commands, BinOp}; grammar; @@ -33,6 +33,7 @@ extern { "load" => Token::Load, "principal" => Token::Principal, "call" => Token::Call, + "encode" => Token::Encode, "config" => Token::Config, "show" => Token::Show, "assert" => Token::Assert, @@ -62,16 +63,28 @@ pub Commands: Commands = SepBy => Commands(<>); pub Command: Command = { "call" > "."? => { let canister = Spanned { span: canister.1.clone(), value: canister.0 }; - Command::Call{canister, method: "".to_string(), args: Vec::new()} + Command::Call{canister, method: "".to_string(), args: Vec::new(), encode_only: false} }, "call" > "." => { let canister = Spanned { span: canister.1.clone(), value: canister.0 }; - Command::Call{canister, method, args: Vec::new()} + Command::Call{canister, method, args: Vec::new(), encode_only: false} }, "call" > "." "(" > ")"? => { let canister = Spanned { span: canister.1.clone(), value: canister.0 }; - Command::Call{canister, method, args} + Command::Call{canister, method, args, encode_only: false} }, + "encode" > "."? => { + let canister = Spanned { span: canister.1.clone(), value: canister.0 }; + Command::Call{canister, method: "".to_string(), args: Vec::new(), encode_only: true} + }, + "encode" > "." => { + let canister = Spanned { span: canister.1.clone(), value: canister.0 }; + Command::Call{canister, method, args: Vec::new(), encode_only: true} + }, + "encode" > "." "(" > ")"? => { + let canister = Spanned { span: canister.1.clone(), value: canister.0 }; + Command::Call{canister, method, args, encode_only: true} + }, "config" => Command::Config(<>), "show" => Command::Show(<>), "assert" "==" => Command::Assert(BinOp::Equal, left, right), @@ -88,7 +101,7 @@ pub Command: Command = { } Value: Value = { - Arg => Value::Candid(<>), + Arg => <>, )*> => { let mut res = Vec::with_capacity(vs.len() + 1); res.push(v); @@ -96,34 +109,35 @@ Value: Value = { Value::Path(res) }, "file" => Value::Blob(<>), + "encode" => Value::Args(<>), } Var: String = { "id" => <>, } -// Value -Args: IDLArgs = "(" > ")" => IDLArgs { args: <> }; +// Candid Value +Values: Vec = "(" > ")" => <>; -Arg: IDLValue = { - "bool" => IDLValue::Bool(<>), +Arg: Value = { + "bool" => Value::Bool(<>), NumLiteral => <>, - Text => IDLValue::Text(<>), + Text => Value::Text(<>), Bytes => { - let values: Vec<_> = <>.into_iter().map(|v| IDLValue::Nat8(v)).collect(); - IDLValue::Vec(values) + let values: Vec<_> = <>.into_iter().map(Value::Nat8).collect(); + Value::Vec(values) }, - "null" => IDLValue::Null, - "opt" => IDLValue::Opt(Box::new(<>)), - "vec" "{" > "}" => IDLValue::Vec(<>), + "null" => Value::Null, + "opt" => Value::Opt(Box::new(<>)), + "vec" "{" > "}" => Value::Vec(<>), "record" "{" >> "}" =>? { let mut id: u32 = 0; let span = <>.1.clone(); - let mut fs: Vec = <>.0.into_iter().map(|f| { + let mut fs: Vec = <>.0.into_iter().map(|f| { match f.id { Label::Unnamed(_) => { id = id + 1; - IDLField { id: Label::Unnamed(id - 1), val: f.val } + Field { id: Label::Unnamed(id - 1), val: f.val } } _ => { id = f.id.get_id() + 1; @@ -131,16 +145,16 @@ Arg: IDLValue = { } } }).collect(); - fs.sort_unstable_by_key(|IDLField { id, .. }| id.get_id()); + fs.sort_unstable_by_key(|Field { id, .. }| id.get_id()); check_unique(fs.iter().map(|f| &f.id)).map_err(|e| error2(e, span))?; - Ok(IDLValue::Record(fs)) + Ok(Value::Record(fs)) }, - "variant" "{" "}" => IDLValue::Variant(VariantValue(Box::new(<>), 0)), - "principal" > =>? Ok(IDLValue::Principal(Principal::from_text(&<>.0).map_err(|e| error2(e, <>.1))?)), - "service" > =>? Ok(IDLValue::Service(Principal::from_text(&<>.0).map_err(|e| error2(e, <>.1))?)), + "variant" "{" "}" => Value::Variant(Box::new(<>), 0), + "principal" > =>? Ok(Value::Principal(Principal::from_text(&<>.0).map_err(|e| error2(e, <>.1))?)), + "service" > =>? Ok(Value::Service(Principal::from_text(&<>.0).map_err(|e| error2(e, <>.1))?)), "func" > "." =>? { let id = Principal::from_text(&id.0).map_err(|e| error2(e, id.1))?; - Ok(IDLValue::Func(id, meth)) + Ok(Value::Func(id, meth)) }, "(" ")" => <>, } @@ -164,22 +178,22 @@ Number: String = { // "hex" => num_bigint::BigInt::parse_bytes(<>.as_bytes(), 16).unwrap().to_str_radix(10), } -AnnVal: IDLValue = { - => <>, - > ":" > =>? { +AnnVal: Value = { + => <>, + ":" > =>? { let env = TypeEnv::new(); let typ = env.ast_to_type(&typ.0).map_err(|e| error2(e, typ.1))?; - arg.0.annotate_type(true, &env, &typ).map_err(|e| error2(e, arg.1)) + Ok(Value::AnnVal(Box::new(arg), typ)) } } -NumLiteral: IDLValue = { +NumLiteral: Value = { => { let num = match sign { Some('-') => format!("-{}", n), _ => n, }; - IDLValue::Number(num) + Value::Number(num) }, > =>? { let span = n.1.clone(); @@ -188,7 +202,7 @@ NumLiteral: IDLValue = { _ => n.0, }; let f = num.parse::().map_err(|_| error2("not a float", span))?; - Ok(IDLValue::Float64(f)) + Ok(Value::Float64(f)) }, } @@ -197,20 +211,20 @@ FieldId: u32 = { Sp<"hex"> =>? u32::from_str_radix(&<>.0, 16).map_err(|_| error2("field id out of u32 range", <>.1)), } -Field: IDLField = { - "=" =>? Ok(IDLField { id: Label::Id(n), val: v }), - "=" => IDLField { id: Label::Named(n), val: v }, +Field: Field = { + "=" =>? Ok(Field { id: Label::Id(n), val: v }), + "=" => Field { id: Label::Named(n), val: v }, } -VariantField: IDLField = { +VariantField: Field = { Field => <>, - Name => IDLField { id: Label::Named(<>), val: IDLValue::Null }, - FieldId =>? Ok(IDLField { id: Label::Id(<>), val: IDLValue::Null }), + Name => Field { id: Label::Named(<>), val: Value::Null }, + FieldId =>? Ok(Field { id: Label::Id(<>), val: Value::Null }), } -RecordField: IDLField = { +RecordField: Field = { Field => <>, - AnnVal => IDLField { id: Label::Unnamed(0), val:<> }, + AnnVal => Field { id: Label::Unnamed(0), val:<> }, } // Common util diff --git a/src/helper.rs b/src/helper.rs index e77f706..6f4f016 100644 --- a/src/helper.rs +++ b/src/helper.rs @@ -1,4 +1,5 @@ -use crate::command::{extract_canister, Value}; +use crate::command::extract_canister; +use crate::value::Value; use ansi_term::Color; use candid::{ check_prog, diff --git a/src/main.rs b/src/main.rs index e4aa2f9..fc48940 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,6 +9,7 @@ mod error; mod grammar; mod helper; mod token; +mod value; use crate::command::Command; use crate::error::pretty_parse; use crate::helper::MyHelper; diff --git a/src/token.rs b/src/token.rs index d18ec5d..00f8900 100644 --- a/src/token.rs +++ b/src/token.rs @@ -56,6 +56,8 @@ pub enum Token { Opt, #[token("call")] Call, + #[token("encode")] + Encode, #[token("config")] Config, #[token("show")] diff --git a/src/value.rs b/src/value.rs new file mode 100644 index 0000000..3bef659 --- /dev/null +++ b/src/value.rs @@ -0,0 +1,105 @@ +use super::command::resolve_path; +use super::helper::MyHelper; +use anyhow::{anyhow, Context}; +use candid::{ + parser::value::{IDLArgs, IDLField, IDLValue, VariantValue}, + types::{Label, Type}, + Principal, TypeEnv, +}; +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub enum Value { + Path(Vec), + Blob(String), + AnnVal(Box, Type), + Args(Vec), + // from IDLValue without the infered types + Nat8 + Bool(bool), + Null, + Text(String), + Number(String), // Undetermined number type + Nat8(u8), + Float64(f64), + Opt(Box), + Vec(Vec), + Record(Vec), + Variant(Box, u64), // u64 represents the index from the type, defaults to 0 when parsing + Principal(Principal), + Service(Principal), + Func(Principal, String), +} +#[derive(Debug, Clone)] +pub struct Field { + pub id: Label, + pub val: Value, +} +impl Value { + pub fn eval(self, helper: &MyHelper) -> anyhow::Result { + Ok(match self { + Value::Path(vs) => helper + .env + .0 + .get(&vs[0]) // TODO handle path + .ok_or_else(|| anyhow!("Undefined variable {}", vs[0]))? + .clone(), + Value::Blob(file) => { + let path = resolve_path(&helper.base_path, PathBuf::from(file)); + let blob: Vec = std::fs::read(&path) + .with_context(|| format!("Cannot read {:?}", path))? + .into_iter() + .map(IDLValue::Nat8) + .collect(); + IDLValue::Vec(blob) + } + Value::AnnVal(v, ty) => { + let arg = v.eval(helper)?; + let env = TypeEnv::new(); + arg.annotate_type(true, &env, &ty)? + } + Value::Args(args) => { + let mut res = Vec::with_capacity(args.len()); + for arg in args.into_iter() { + res.push(arg.eval(helper)?); + } + let args = IDLArgs { args: res }; + let bytes = args.to_bytes()?; + IDLValue::Vec(bytes.into_iter().map(IDLValue::Nat8).collect()) + } + Value::Bool(b) => IDLValue::Bool(b), + Value::Null => IDLValue::Null, + Value::Text(s) => IDLValue::Text(s), + Value::Nat8(n) => IDLValue::Nat8(n), + Value::Number(n) => IDLValue::Number(n), + Value::Float64(f) => IDLValue::Float64(f), + Value::Principal(id) => IDLValue::Principal(id), + Value::Service(id) => IDLValue::Service(id), + Value::Func(id, meth) => IDLValue::Func(id, meth), + Value::Opt(v) => IDLValue::Opt(Box::new((*v).eval(helper)?)), + Value::Vec(vs) => { + let mut vec = Vec::with_capacity(vs.len()); + for v in vs.into_iter() { + vec.push(v.eval(helper)?); + } + IDLValue::Vec(vec) + } + Value::Record(fs) => { + let mut res = Vec::with_capacity(fs.len()); + for Field { id, val } in fs.into_iter() { + res.push(IDLField { + id, + val: val.eval(helper)?, + }); + } + IDLValue::Record(res) + } + Value::Variant(f, idx) => { + let f = IDLField { + id: f.id, + val: f.val.eval(helper)?, + }; + IDLValue::Variant(VariantValue(Box::new(f), idx)) + } + }) + } +}