Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Misc improvements #94

Merged
merged 13 commits into from
Jul 29, 2024
691 changes: 362 additions & 329 deletions Cargo.lock

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "ic-repl"
version = "0.7.3"
version = "0.7.4"
authors = ["DFINITY Team"]
edition = "2021"
default-run = "ic-repl"
Expand All @@ -24,10 +24,10 @@ codespan-reporting = "0.11"
pretty = "0.12"
pem = "3.0"
shellexpand = "3.1"
ic-agent = "0.35"
ic-identity-hsm = "0.35"
ic-transport-types = "0.35"
ic-wasm = { version = "0.7", default-features = false }
ic-agent = "0.37"
ic-identity-hsm = "0.37"
ic-transport-types = "0.37"
ic-wasm = { version = "0.8", default-features = false }
inferno = { version = "0.11", default-features = false, features = ["multithreaded", "nameattr"] }
tokio = { version = "1.35", features = ["full"] }
anyhow = "1.0"
Expand Down
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
# Canister REPL

```
ic-repl [--replica [local|ic|url] | --offline [--format [json|ascii|png]]] --config <toml config> [script file]
ic-repl [--replica [local|ic|url] | --offline [--format [json|ascii|png]]] --config <toml config> [script file] --verbose
```

## Commands

```
<command> :=
| import <id> = <text> (as <text>)? // bind canister URI to <id>, with optional did file
| export <text> // export current environment variables
| load <text> // load and run a script file
| load <exp> // load and run a script file. Do not error out if <exp> ends with '?'
| config <text> // set config in TOML format
| let <id> = <exp> // bind <exp> to a variable <id>
| <exp> // show the value of <exp>
Expand Down Expand Up @@ -50,8 +49,10 @@ We also provide some built-in functions:
* `neuron_account(principal, nonce)`: convert (principal, nonce) to account in the governance canister.
* `file(path)`: load external file as a blob value.
* `gzip(blob)`: gzip a blob value.
* `replica_url()`: returns the replica URL ic-repl connects to.
* `stringify(exp1, exp2, exp3, ...)`: convert all expressions to string and concat. Only supports primitive types.
* `output(path, content)`: append text content to file path.
* `export(path, var1, var2, ...)`: overwrite variable bindings to file path. The file can be used by the `load` command.
* `wasm_profiling(path)/wasm_profiling(path, record { trace_only_funcs = <vec text>; start_page = <nat>; page_limit = <nat> })`: load Wasm module, instrument the code and store as a blob value. Calling profiled canister binds the cost to variable `__cost_{id}` or `__cost__`. The second argument is optional, and all fields in the record are also optional. If provided, `trace_only_funcs` will only count and trace the provided set of functions; `start_page` writes the logs to a preallocated pages in stable memory; `page_limit` specifies the number of the preallocated pages, default to 4096 if omitted. See [ic-wasm's doc](https://github.com/dfinity/ic-wasm#working-with-upgrades-and-stable-memory) for more details.
* `flamegraph(canister_id, title, filename)`: generate flamegraph for the last update call to canister_id, with title and write to `{filename}.svg`. The cost of the update call is returned.
* `concat(e1, e2)`: concatenate two vec/record/text together.
Expand All @@ -61,6 +62,7 @@ We also provide some built-in functions:
* `and/or(e1, e2)/not(e)`: logical and/or/not.
* `exist(e)`: check if `e` can be evaluated without errors. This is useful to check the existence of data, e.g., `exist(res[10])`.
* `ite(cond, e1, e2)`: expression version of conditional branch. For example, `ite(exist(res.ok), "success", "error")`.
* `exec(cmd, arg1, arg2, ...)/exec(cmd, arg1, arg2, ..., record { silence = <bool>; cwd = <text> })`: execute a bash command. The arguments are all text types. The last line from stdout is parsed by the Candid value parser as the result of the `exec` function. If parsing fails, returns that line as a text value. You can specify an optional record argument at the end. All fields in the record are optional. If provided, `silence = true` hides the stdout and stderr output; `cwd` specifies the current working directory of the command. There are security risks in running arbitrary bash command. Be careful about what command you execute.

The following functions are only available in non-offline mode:
* `read_state([effective_id,] prefix, id, paths, ...)`: fetch the state tree path of `<prefix>/<id>/<paths>`. Some useful examples,
Expand All @@ -72,6 +74,15 @@ The following functions are only available in non-offline mode:
+ node public key: `read_state("subnet", principal "subnet_id", "node", principal "node_id", "public_key")`
* `send(blob)`: send signed JSON messages generated from offline mode. The function can take a single message or an array of messages. Most likely use is `send(file("messages.json"))`. The return result is the return results of all calls. Alternatively, you can use `ic-repl -s messages.json -r ic`.

There is a special `__main` function you can define in the script, which gets executed when loading from CLI. `__main` can take arguments provided from CLI. The CLI arguments gets parsed by the Candid value parser first. If parsing fails, it is stored as a text value. For example, the following code can be called with `ic-repl main.sh -- test 42` and outputs "test43".

### main.sh
```
function __main(name, n) {
stringify(name, add(n, 1))
}
```

## Object methods

For `vec`, `record` or `text` value, we provide some built-in methods for value transformation:
Expand Down
2 changes: 2 additions & 0 deletions examples/func.sh
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ function fib3(n) {
let _ = add(fib3(sub(n, 1)), fib3(sub(n, 2)));
}
};
function __main() {
assert fac(5) == 120;
assert fac2(5) == 120;
assert fac3(5) == 120;
assert fib(10) == 89;
assert fib2(10) == 89;
assert fib3(10) == 89;
}
57 changes: 32 additions & 25 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ pub enum Command {
Show(Exp),
Let(String, Exp),
Assert(BinOp, Exp, Exp),
Export(String),
Import(String, Principal, Option<String>),
Load(String),
Load(Exp),
Identity(String, IdentityConfig),
Func {
name: String,
Expand Down Expand Up @@ -111,8 +110,10 @@ impl Command {
let v = val.eval(helper)?;
let duration = time.elapsed();
bind_value(helper, "_".to_string(), v, is_call, true);
let width = console::Term::stdout().size().1 as usize;
println!("{:>width$}", format!("({duration:.2?})"), width = width);
if helper.verbose {
let width = console::Term::stdout().size().1 as usize;
println!("{:>width$}", format!("({duration:.2?})"), width = width);
}
}
Command::Identity(id, config) => {
use ic_agent::identity::{BasicIdentity, Identity, Secp256k1Identity};
Expand Down Expand Up @@ -164,31 +165,36 @@ impl Command {
helper.current_identity = id.to_string();
helper.env.0.insert(id, IDLValue::Principal(sender));
}
Command::Export(file) => {
use std::io::{BufWriter, Write};
let path = resolve_path(&std::env::current_dir()?, &file);
let file = std::fs::File::create(path)?;
let mut writer = BufWriter::new(&file);
for (id, val) in helper.env.0.iter() {
writeln!(&mut writer, "let {id} = {val};")?;
}
}
Command::Load(file) => {
Command::Load(e) => {
// TODO check for infinite loop
// Note that it's a bit tricky to make load as a built-in function, as it requires mutable access to helper.
let IDLValue::Text(file) = e.eval(helper)? else {
return Err(anyhow!("load needs to be a file path"));
};
let (file, fail_safe) = if file.ends_with('?') {
(file.trim_end_matches('?'), true)
} else {
(file.as_str(), false)
};
let old_base = helper.base_path.clone();
let path = resolve_path(&old_base, &file);
let mut script = std::fs::read_to_string(&path)
.with_context(|| format!("Cannot read {path:?}"))?;
let path = resolve_path(&old_base, file);
let read_result = std::fs::read_to_string(&path);
if read_result.is_err() && fail_safe {
return Ok(());
}
let mut script = read_result.with_context(|| format!("Cannot read {path:?}"))?;
if script.starts_with("#!") {
let line_end = script.find('\n').unwrap_or(0);
script.drain(..line_end);
}
let script =
shellexpand::env(&script).map_err(|e| crate::token::error2(e, 0..0))?;
let cmds = pretty_parse::<Commands>(&file, &script)?;
let cmds = pretty_parse::<Commands>(file, &script)?;
helper.base_path = path.parent().unwrap().to_path_buf();
for (cmd, pos) in cmds.0.into_iter() {
println!("> {}", &script[pos]);
if helper.verbose {
println!("> {}", &script[pos]);
}
cmd.run(helper)?;
}
helper.base_path = old_base;
Expand Down Expand Up @@ -239,20 +245,21 @@ impl std::str::FromStr for Commands {
}

fn bind_value(helper: &mut MyHelper, id: String, v: IDLValue, is_call: bool, display: bool) {
if display {
if helper.verbose {
println!("{v}");
} else if let IDLValue::Text(v) = &v {
println!("{v}");
}
}
if is_call {
let (v, cost) = crate::profiling::may_extract_profiling(v);
if let Some(cost) = cost {
let cost_id = format!("__cost_{id}");
helper.env.0.insert(cost_id, IDLValue::Int64(cost));
}
if display {
println!("{v}");
}
helper.env.0.insert(id, v);
} else {
if display {
println!("{v}");
}
helper.env.0.insert(id, v);
}
}
Loading
Loading