-
Notifications
You must be signed in to change notification settings - Fork 119
fix: catch up with Candid spec changes (reserved ↛ null)
#5619
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
base: claudio/issue-5504
Are you sure you want to change the base?
Conversation
Flake lock file updates:
• Updated input 'candid-src':
'github:dfinity/candid/1725ea211e928a4dc18c4147d27ed5764f00df5c?narHash=sha256-7/aBuCJUtH5nofVfgyedqI6t7a21H%2B14kwmYOGFbO8g%3D' (2025-10-02)
→ 'github:dfinity/candid/4e8357d17198e53b4d426636a0b70d1433b40390?narHash=sha256-lRjdOwUF06g04qjUhFVU19sjYZooSHEZa3cKcpiZHYQ%3D' (2025-10-16)
• Updated input 'ic-wasm-src':
'github:dfinity/ic-wasm/d61f15ea363eebb952195f57cf9c5db3b2cade21?narHash=sha256-PTwtdFVGGofm934tlikb2cNq932yuBjXYtN5TKXfwmg%3D' (2025-10-01)
→ 'github:dfinity/ic-wasm/10f1b5965c8c4b7fd1532734b60dcb2a2fcb2673?narHash=sha256-9vwXW4NTJzdwQUPk5%2BvHp0uWFJr6o3/Pj/wZ26gTyfM%3D' (2025-10-22)
• Updated input 'motoko-core-src':
'github:dfinity/motoko-core/c49a060df3e066463d81664f27065238ca7cc23f?narHash=sha256-INchg3S6uo8qKUvShjHmGVhgKq2FdQkcYKJkE2GLDoY%3D' (2025-10-14)
→ 'github:dfinity/motoko-core/c707f40fe514bbde9a33869e71a86c6d36ce7e32?narHash=sha256-knLQ0jGi0OcZTEmhRFSaUjqQkrOJZ4eGKJhGc3o%2B8IE%3D' (2025-10-28)
> warning [M0215], field `_1_` is provided but not expected in record of type
{_0_ : Null}
> assert blob "DIDL\00\02\7d\7d\05\06" == "(5, 6)" : (nat, nat, null) "parsing a top-level tuple into a longer top-level tuple"; current failure: > construct:215 parsing reserved as null ... not ok (unwanted pass)!
To wit:
```
let ?((null, 5), null) : ?((Null, Nat), Null) = from_candid "DIDL\01\6C\02\00\70\01\7D\02\00\7C\05\7A" else break good;
```
the first `Null` gets derailed by `\70` (`Reserved` singleton), but the second `Null` is unaffected by
the `int` (-6).
Changing `\70` back to `\7F` (i.e. `null`), gives acceptance.
reserved to null)reserved ↛ null)
src/mo_idl/idl_to_mo_value.ml
Outdated
|
|
||
| let args vs ts = parens_comma (List.map2 value vs.it ts) | ||
| let rec args vs = function | ||
| | ts when List.(compare_lengths vs.it ts < 0 && for_all null (Lib.List.drop (length vs.it) ts)) -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extends values to be checked when shorter than types and those are all Null.
| | ts when List.(compare_lengths vs.it ts < 0 && for_all null (Lib.List.drop (length vs.it) ts)) -> | ||
| let vs' = vs.it @ Lib.List.replicate { vs with it = NullV } List.(length ts - length vs.it) in | ||
| args {vs with it = vs'} ts | ||
| | ts when List.(exists (fun (t, v) -> apart t v.it) (combine ts vs.it)) -> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adds null-ed fields when the object type calls for them.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In
assert blob "DIDL\01\6c\01\01\7d\01\00\05" == "(record { 1 = 5 })" : (record { 0 : null }) "parsing into record with expected field that is less than extra field on the wire";
The record { 1 = 5 } fragment need an augmenting to record { 1 = 5; 0 = null } so that the Motoko generation creates type-correct code.
| List.fold_left | ||
| (fun s c -> add (mul s (of_int 223)) (of_int (Char.code c))) | ||
| zero | ||
| (* TODO: also unescape the string, once #465 is approved *) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was actually resolved back then.
| | Prim Null | Opt _ | Any -> | ||
| (Bool.lit true, fun msg -> Opt.null_lit env) | ||
| | Prim Null -> | ||
| (get_can_recover, null_result, fun _ -> compile_unboxed_const (coercion_error_value env)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't quite grok why this case is different. Can you explain?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, the spec says that every Candid value is a subtype of opt T, and gives opt t when t <: T. But the null type has no subtypes. I.e. we only get out a null : Null when the binary Candid has a 0x7F there. In all other cases the coercion must fail, either trapping or indicating a coercion failure to the caller (when recoverable). However null : ?T can always be materialised when an argument is missing, similarly null : Null. So we have to treat missing arguments and coercion failures differently when the requested type is Null (or null respectively, in Candid).
These are coercion failures:
assert blob "DIDL\00\01\70" !: (null) "parsing reserved as null";
assert blob "DIDL\00\02\70\70" !: (null, null) "parsing (reserved, reserved) as (null, null)";
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though, I must admit, I cannot really wrap my head around this one. @mraszyk can you give a hint? (The test always passed, so just quenching my curiosity.)
// parsing reserved as null
assert blob "DIDL\00\01\70" == "(null)" : (reserved) "reserved";
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These test the null-materialisation for missing top-level args, i.e. argument_default_or_trap:
assert blob "DIDL\00\00" == "()" : (null) "parsing an empty top-level tuple into a longer top-level tuple";
assert blob "DIDL\00\02\7d\7d\05\06" == "(5, 6)" : (nat, nat, null) "parsing a top-level tuple into a longer top-level tuple";
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// parsing reserved as null
assert blob "DIDL\00\01\70" == "(null)" : (reserved) "reserved";
Just to clarify, it think the comment should reading // parsing reserved as null value of type reserved, if that helps any.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
\70 is a value of type reserved, so I don't see how a null appears suddenly in the game...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would be good to add a PR description.
| debug.print: {o1 = {x = null}; o2 = {x = null}; o3 = {x = null}} | ||
| debug.print: {o1 = {x = null}; o2 = {x = ?null}; o3 = {x = ?(?null)}} | ||
| ingress Completed: Reply: 0x4449444c0000 | ||
| ingress Err: IC0503: Error from Canister rwlgt-iiaaa-aaaaa-aaaaa-cai: Canister called `ic0.trap` with message: 'IDL error: unexpected IDL type when parsing Null'. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are totally expected. A <reserved> marker (i.e. \x70 = -16) is not digestible by null : Null pattern, similarly a 5 : nat (here x7D\x05).
| Unix.putenv "MOC_UNLOCK_PRIM" "yesplease"; | ||
| write_file "tmp.mo" src; | ||
| match run_cmd "moc -Werror -wasi-system-api tmp.mo -o tmp.wasm" with | ||
| match run_cmd "moc -A M0215 -Werror -wasi-system-api tmp.mo -o tmp.wasm" with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Motoko code generated by these
assert blob "DIDL\01\6c\01\00\7d\01\00\05" == "(record { 0 = 5 })" : (record { 1 : null }) "parsing into record with expected field that is greater than extra field on the wire";
assert blob "DIDL\01\6c\01\01\7d\01\00\05" == "(record { 1 = 5 })" : (record { 0 : null }) "parsing into record with expected field that is less than extra field on the wire";
would contain dead fields, which cause these warnings. I couldn't come up with a massaging so that the warning goes away, that doesn't change the meaning of the test. So I have just suppressed the warning so that -Werror won't kill compilation.
| args {vs with it = vs'} ts | ||
| | ts when List.(exists (fun (t, v) -> apart t v.it) (combine ts vs.it)) -> | ||
| args {vs with it = List.map2 enrich ts vs.it} ts | ||
| | ts -> parens_comma (List.map2 value vs.it ts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the vanilla case, and we should exit here after all massaging from above is done.
src/mo_idl/idl_to_mo_value.ml
Outdated
| | ts when List.(exists (fun (t, v) -> apart t v.it) (combine ts vs.it)) -> | ||
| args {vs with it = List.map2 enrich ts vs.it} ts | ||
| | ts -> parens_comma (List.map2 value vs.it ts) | ||
| and null t = t = T.(Prim Null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
rename to is_null
| | RecordV fs, T.(Obj (Object, tfs)) -> | ||
| "{" ^ String.concat "; " (List.map (fun f -> | ||
| Idl_to_mo.check_label (fst f.it) ^ " = " ^ value (snd f.it) (find_typ tfs f) | ||
| Idl_to_mo.check_label (fst f.it) ^ " = " ^ value (snd f.it) (find_typ ~infer:infer_typ tfs f) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This deals with previously errored (and ignored) test cases
construct:233 parsing into record with expected field that is greater than extra field on the wire ... ignored ((string):1.11-1.16: import error [M0164], unknown record or variant label in textual representation
)
construct:234 parsing into record with expected field that is less than extra field on the wire ... ignored ((string):1.11-1.16: import error [M0164], unknown record or variant label in textual representation
arising from
assert blob "DIDL\01\6c\01\00\7d\01\00\05" == "(record { 0 = 5 })" : (record { 1 : null }) "parsing into record with expected field that is greater than extra field on the wire";
assert blob "DIDL\01\6c\01\01\7d\01\00\05" == "(record { 1 = 5 })" : (record { 0 : null }) "parsing into record with expected field that is less than extra field on the wire";
| | ts when List.(exists (fun (t, v) -> apart t v.it) (combine ts vs.it)) -> | ||
| args {vs with it = List.map2 enrich ts vs.it} ts | ||
| | ts -> parens_comma (List.map2 value vs.it ts) | ||
| and is_null t = t = T.(Prim Null) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| and is_null t = t = T.(Prim Null) | |
| and is_null_opt_reserved t = |
Should this maybe allow Null, Opt t an Any (after normalizing)?
| | _ -> raise (UnsupportedCandidFeature | ||
| (Diag.error_message v.at "M0165" "import" "odd expected type")) | ||
|
|
||
| let args vs ts = parens_comma (List.map2 value vs.it ts) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this code might be simpler if you just traversed the typs in ts and only allow trailing defaultable types?
| args {vs with it = List.map2 enrich ts vs.it} ts | ||
| | ts -> parens_comma (List.map2 value vs.it ts) | ||
| and is_null t = t = T.(Prim Null) | ||
| and apart t v = match t, v with |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto, this code might be simpler if you sorted the value and type fields by hash and then traverse the (type) fields in order, inserting preserving fields and inserting null fields when the type is defualtable.
Not sure.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. Thanks for tackling this!
Reproduce issue #5543. Fixes #5543.
The spec says that every Candid value is a subtype of
opt T, and givesopt twhent <: T. But thenulltype has no subtypes. I.e. we only get out anull : Nullwhen the binary Candid has a0x7Fthere. In all other cases the coercion must fail, either trapping or indicating a coercion failure to the caller (when recoverable). Howevernull : ?Tcan always be materialised when an argument is missing, similarlynull : Null. So we have to treat missing arguments and coercion failures differently when the requested type isNull(ornullrespectively, in Candid).This PR corrects the reading of (e.g.
\x70,<reserved>) in the non-recoverable case and makes it a coercion failure.Also adds improvements to the
candid-testsrunner toM0164, ignored test)nullvalues to top-level argument tuples where the type requests themnullfields to record values (in argument sequences) where the type requests them