Skip to content

Commit

Permalink
Add control flow and binop functions (#90)
Browse files Browse the repository at this point in the history
* infer effective id for subnet nodes

* add eq/neq/lt/lte/gt/gte/and/or/not

* bump

* add ite

* index can take exp

* add while, if

* allow error cond to be false

* add exist to avoid footgun

* bump
  • Loading branch information
chenyan-dfinity authored Apr 11, 2024
1 parent b4e9818 commit 4a24c21
Show file tree
Hide file tree
Showing 10 changed files with 493 additions and 254 deletions.
372 changes: 186 additions & 186 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.0"
version = "0.7.1"
authors = ["DFINITY Team"]
edition = "2021"
default-run = "ic-repl"
Expand All @@ -24,9 +24,9 @@ codespan-reporting = "0.11"
pretty = "0.12"
pem = "3.0"
shellexpand = "3.1"
ic-agent = "0.33"
ic-identity-hsm = "0.33"
ic-transport-types = "0.33"
ic-agent = "0.34"
ic-identity-hsm = "0.34"
ic-transport-types = "0.34"
ic-wasm = { version = "0.7", default-features = false }
inferno = { version = "0.11", default-features = false, features = ["multithreaded", "nameattr"] }
tokio = { version = "1.35", features = ["full"] }
Expand All @@ -35,7 +35,7 @@ rand = "0.8"
logos = "0.13"
lalrpop-util = "0.20"
clap = { version = "4.4", features = ["derive"] }
ring = "0.16"
ring = "0.17"
rpassword = "7.2"
serde = "1.0"
serde_json = "1.0"
Expand Down
60 changes: 46 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ ic-repl [--replica [local|ic|url] | --offline [--format [json|ascii|png]]] --con

```
<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
| config <text> // set config for random value generator in dhall format
| let <id> = <exp> // bind <exp> to a variable <id>
| <exp> // show the value of <exp>
| assert <exp> <binop> <exp> // assertion
| 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
| config <text> // set config for random value generator in dhall format
| let <id> = <exp> // bind <exp> to a variable <id>
| <exp> // show the value of <exp>
| assert <exp> <binop> <exp> // assertion
| identity <id> (<text> | record { slot_index = <nat>; key_id = <text> })? // switch to identity <id>, with optional pem file or HSM config
| function <id> ( <id>,* ) { <command>;* } // define a function
| function <id> ( <id>,* ) { <command>;* } // define a function
| if <exp> { <command>;* } else { <command>;* } // conditional branch
| while <exp> { <command>;* } // while loop
<exp> :=
| <candid val> // any candid value
| <var> <transformer>* // variable with optional transformers
Expand All @@ -31,7 +33,7 @@ ic-repl [--replica [local|ic|url] | --offline [--format [json|ascii|png]]] --con
<transformer> :=
| ? // select opt value
| . <name> // select field name from record or variant value
| [ <nat> ] // select index from vec, record, or variant value
| [ <exp> ] // select index from vec, text, record, or variant value
| . <id> ( <exp>,* ) // transform (map, filter, fold) a collection value
<binop> :=
| == // structural equality
Expand All @@ -42,7 +44,6 @@ ic-repl [--replica [local|ic|url] | --offline [--format [json|ascii|png]]] --con
## Functions

Similar to most shell languages, functions in ic-repl is dynamically scoped and untyped.
You cannot define recursive functions, as there is no control flow in the language.

We also provide some built-in functions:
* `account(principal)`: convert principal to account id.
Expand All @@ -54,19 +55,23 @@ We also provide some built-in functions:
* `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.
* `add/sub/mul/div(e1, e2)`: addition/subtraction/multiplication/division of two integer/float numbers. If one of the arguments is float32/float64, the result is float64; otherwise, the result is integer. You can use type annotation to get the integer part of the float number. For example `div((mul(div(1, 3.0), 1000) : nat), 100.0)` returns `3.33`.
* `add/sub/mul/div(e1, e2)`: addition/subtraction/multiplication/division of two integers/floats. If one of the arguments is float32/float64, the result is float64; otherwise, the result is integer. You can use type annotation to get the integer part of the float number. For example `div((mul(div(1, 3.0), 1000) : nat), 100.0)` returns `3.33`.
* `lt/lte/gt/gte(e1, e2)`: check if integer/float `e1` is less than/less than or equal to/greater than/greater than or equal to `e2`.
* `eq/neq(e1, e2)`: check if `e1` and `e2` are equal or not. `e1` and `e2` must have the same type.
* `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")`.

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,
+ candid metadata: `read_state("canister", principal "canister_id", "metadata/candid:service")`
+ canister controllers: `read_state("canister", principal "canister_id", "controllers")`
+ list all subnet ids: `read_state("subnet")`
+ subnet metrics: `read_state("subnet", principal "subnet_id", "metrics")`
+ list subnet nodes: `read_state(principal "effective_canister_id", "subnet", principal "subnet_id", "node")`
+ node public key: `read_state(principal "effective_canister_id", "subnet", principal "subnet_id", "node", principal "node_id", "public_key")`
+ list subnet nodes: `read_state("subnet", principal "subnet_id", "node")`
+ 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`.


## Object methods

For `vec`, `record` or `text` value, we provide some built-in methods for value transformation:
Expand Down Expand Up @@ -209,6 +214,33 @@ flamegraph(cid, "hashmap.put(50)", "put.svg");
output(file, stringify("[", __cost_put, "](put.svg)|\n"));
```

### recursion.sh
```
function fib(n) {
let _ = ite(lt(n, 2), 1, add(fib(sub(n, 1)), fib(sub(n, 2))))
};
function fib2(n) {
let a = 1;
let b = 1;
while gt(n, 0) {
let b = add(a, b);
let a = sub(b, a);
let n = sub(n, 1);
};
let _ = a;
};
function fib3(n) {
if lt(n, 2) {
let _ = 1;
} else {
let _ = add(fib3(sub(n, 1)), fib3(sub(n, 2)));
}
};
assert fib(10) == 89;
assert fib2(10) == 89;
assert fib3(10) == 89;
```

## Relative paths

Several commands and functions are taking arguments from the file system. We have different definitions for
Expand Down
63 changes: 60 additions & 3 deletions examples/func.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ function f(x) {
let _ = x.id;
};
function f2(x) { let _ = record { abc = x.id; } };
function f3(x) { let _ = x.y };
function f3(x) { let _ = exist(x.y) };
function f3_2(x) { let _ = x.y };
function f4(acc, x) { let _ = add(acc, x) };
let x = vec{record {id=1;x=opt 2};record {id=2;y=opt 5}};
assert x.map(f) == vec {1;2};
Expand All @@ -12,29 +13,85 @@ assert x.filter(f3).map(f) == vec {2};
assert x.map(f).fold(0, f4) == 3;

let y = vec { variant { y = 1 }; variant { x = "error" }; variant { y = 2 } };
assert y.filter(f3).map(f3) == vec {1;2};
assert y.filter(f3).map(f3_2) == vec {1;2};
assert y[sub(y.size(), 1)].y == 2;

let z = record { opt 1;2;opt 3;opt 4 };
function f5(x) { let _ = record { x[0]; x[1]? } };
function f6(x) { let _ = x[1]? };
function f6(x) { let _ = exist(x[1]?) };
function f7(acc, x) { let _ = concat(acc, vec{x[1]}) };
assert z.filter(f6).map(f5) == record { 1; 2 = 3; 4 };
assert z.filter(f6).map(f5).fold(vec{}, f7) == vec {1;3;4};
assert z[sub(z.size(), 1)]? == 4;

let s = "abcdef";
function f8(x) { let _ = stringify(" ", x) };
function f9(acc, x) { let _ = add(acc, 1) };
assert s.map(f8) == " a b c d e f";
assert s.map(f8).fold(0, f9) == 12;
assert s.map(f8).size() == (12 : nat);
assert s[sub(s.size(), 1)] == "f";

assert div(1, 2) == 0;
assert div(1, 2.0) == 0.5;
assert div((mul(div(((1:nat8):float32), (3:float64)), 1000) : nat), 100.0) == 3.33;
assert eq("text", "text") == true;
assert not(eq("text", "text")) == false;
assert eq(div(1,2), sub(2,2)) == true;
assert gt(div(1, 2.0), 1) == false;
assert eq(div(1, 2.0), 0.5) == true;
assert and(lte(div(1, 2), 0), gte(div(1, 2), 0)) == true;
assert or(lt(div(1, 2), 0), gt(div(1, 2), 0)) == false;

assert (service "aaaaa-aa" : principal) == principal "aaaaa-aa";
assert eq((service "aaaaa-aa" : principal), principal "aaaaa-aa") == true;
assert (func "aaaaa-aa".test : service {}) == service "aaaaa-aa";
assert (principal "aaaaa-aa" : service {}) == service "aaaaa-aa";
assert ("this is a text" : blob) == blob "this is a text";
assert (blob "this is a blob" : text) == "this is a blob";
function fac(n) {
if eq(n, 0) {
let _ = 1;
} else {
let _ = mul(n, fac(sub(n, 1)));
}
};
function fac2(n) {
let res = 1;
while gt(n, 0) {
let res = mul(res, n);
let n = sub(n, 1);
};
let _ = res;
};
function fac3(n) {
let _ = ite(eq(n, 0), 1, mul(n, fac3(sub(n, 1))))
};
function fib(n) {
let _ = ite(lt(n, 2), 1, add(fib(sub(n, 1)), fib(sub(n, 2))))
};
function fib2(n) {
let a = 1;
let b = 1;
while gt(n, 0) {
let b = add(a, b);
let a = sub(b, a);
let n = sub(n, 1);
};
let _ = a;
};
function fib3(n) {
if lt(n, 2) {
let _ = 1;
} else {
let _ = add(fib3(sub(n, 1)), fib3(sub(n, 2)));
}
};
assert fac(5) == 120;
assert fac2(5) == 120;
assert fac3(5) == 120;
assert fib(10) == 89;
assert fib2(10) == 89;
assert fib3(10) == 89;
34 changes: 34 additions & 0 deletions src/command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ pub enum Command {
args: Vec<String>,
body: Vec<Command>,
},
While {
cond: Exp,
body: Vec<Command>,
},
If {
cond: Exp,
then: Vec<Command>,
else_: Vec<Command>,
},
}
#[derive(Debug, Clone)]
pub enum IdentityConfig {
Expand Down Expand Up @@ -174,6 +183,31 @@ impl Command {
}
helper.base_path = old_base;
}
Command::If { cond, then, else_ } => {
let IDLValue::Bool(cond) = cond.eval(helper)? else {
return Err(anyhow!("if condition is not a boolean expression"));
};
if cond {
for cmd in then.into_iter() {
cmd.run(helper)?;
}
} else {
for cmd in else_.into_iter() {
cmd.run(helper)?;
}
}
}
Command::While { cond, body } => loop {
let IDLValue::Bool(cond) = cond.clone().eval(helper)? else {
return Err(anyhow!("while condition is not a boolean expression"));
};
if !cond {
break;
}
for cmd in body.iter() {
cmd.clone().run(helper)?;
}
},
}
Ok(())
}
Expand Down
Loading

0 comments on commit 4a24c21

Please sign in to comment.