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

AST-89 Range restricted Nat* and Int* #487

Merged
merged 70 commits into from
Jun 18, 2019
Merged

AST-89 Range restricted Nat* and Int* #487

merged 70 commits into from
Jun 18, 2019

Conversation

ggreif
Copy link
Contributor

@ggreif ggreif commented Jun 7, 2019

https://dfinity.atlassian.net/browse/AST-89

Conversions work. Tests prove it. There might be better ways of conversion at the 64-bit plane than going via string, but I am not aware of any. I added Maybe we should add to/from_big_int methods to the NumType signature.

Open Issue: Since we don't have representational equivalence (subtyping) between Nat8 and Int8, we can't do the - : Nat8 -> Int8 hack, so we have to annotate like this: -(42 : Int8) to get the effect. There is was an attempt in the code to fix this, but it is was wrong as-is, and will be has been removed unless we find a better way (e.g. automatic insertion of range checks). Same issue applies to binary operators ==, etc.

Laundry list:

  • Unary operators (esp. -), need new hack in typing.ml: - : NatN -> IntN needs revert -- Done
  • -9_223_372_036_854_775_808 : Int64 gives type error, literal out of range for type Int64, needs the sign hack (actually, not.) See open issue above.
  • - intToInt64 (- 2 ** 63 + 1) traps in the wasm, but not in the interpreter (libTomMath, or binding bug?, fixed by @nomeata and incorporated in b4caf31)
  • overflow checks in compilation
  • Serialisation
  • compile literals
  • document and update AST-89
  • hide stack trace on range fail (interpreter)
  • abs, neg corner cases (where it needs to trap)
  • debug_show for Word*, Nat* and Int*
  • investigate diff test/run-dfinity/ok/data-params.dvm-run.ok test/run-dfinity/ok/data-params.run-low.ok (this is now lsb_adjust correct bits when externalising Word8 #502)
  • write issue for interpreter src-loc hack
  • Nat*, Int* and Word* patterns

@ggreif ggreif changed the title Range restricted Nat* and Int* AST-89 Range restricted Nat* and Int* Jun 7, 2019
ggreif added 11 commits June 8, 2019 17:06
and fix msb_adjust for Int/Nat -> IntN/NatN
still needs Int/Nat8/64!
caveat: when using libTomMath binding

these fail:
 - intToInt8 (-128)
 - intToInt16 (-32768)
 -intToInt32 (-2147483648)

The reason is that `fits_signed_bits` is buggy.

(BigNum64 module works in this respect.)
relaxing the fits_signed_bits contract to work with 64
@ggreif ggreif marked this pull request as ready for review June 17, 2019 12:20
Copy link
Contributor

@crusso crusso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM but I haven't reviewed the compiler - leaving that to Joachim.

PS. Part of me wants to redefine the fixed-size numeric literals so we just have three constructors (WordX, IntX and NatX) that take a module giving the interpretation of the type as an argument and a value of that type.
Something that might be expressed in Haskell as:
WordX : forall w :: Word w, w.
NumX: forwall w:: Num w. w.
NumBinOp: forall n :: Numeric n. nbop * exp * exp
LogBinOp: forall n :: Numeric n. lbop * exp * exp
NumUnOp: forall n :: Numeric n. nup * exp
LogNumOp: forall n :: Logical n. lup * exp
And then the interpeter/compiler would use the first-class module/dictionary to interpret/compile accordingly...
But maybe that would be less direct than what we have now.

@@ -720,7 +733,8 @@ let interpret_prog flags scope p : (V.value * scope) option =
let vo = ref None in
let ve = ref V.Env.empty in
Scheduler.queue (fun () ->
interpret_block env p.it (Some ve) (fun v -> vo := Some v)
try interpret_block env p.it (Some ve) (fun v -> vo := Some v)
with Invalid_argument s -> trap !last_region "%s" s
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you still need try … with ... now that you transform Invalid_argument to trap at the interpretation of operations?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only catch some. I could not make catching where CallE is interpreted to work. (It would have broken general tail-recursion.) So this is still needed. In some ideal world we'll have failure continuations where we can feed the exception into, instead of raise.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are the remaining places that raise Invalid_argument? Can't you just handle those, as you do for binop and immediately trap? CallE shouldn't (by itself) trap at all. If you think CallE should turn the failure of v_asfunc into a trap I think that is mistake since that an indicates a type soundness bug, not an expected trap. Or perhaps I'm misunderstanding.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Anyway, its probably not worth worrying about since the scheduler itself doesn't use tailcalls to evaluate its tasks:

module Scheduler =
struct
  let q : (unit -> unit) Queue.t = Queue.create ()

  let queue work = Queue.add work q
  let yield () =
    trace_depth := 0;
    try Queue.take q () with Trap (at, msg) ->
      Printf.printf "%s: execution error, %s\n" (Source.string_of_region at) msg

  let rec run () =
    if not (Queue.is_empty q) then (yield (); run ())
end

That would need fixing first (separately from this), for your try with to make any difference.
Sorry about the distraction...


let prim = function
| "abs" -> fun v k -> k (Int (Nat.abs (as_int v)))

| "Nat8->Word8" -> fun v k ->
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if there's enough uniformity in all these conversions that you could refactor them into a uses of a few single higher order function that takes Numeric modules are arguments. Probably not worth it now you've completed but maybe as a future refactoring...

Copy link
Contributor Author

@ggreif ggreif Jun 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need some sparring on how to use first-class modules, but yeah I was thinking of that. Unfortunately the primitives are not totally homogenous (some details differ). Also the NumType and WordType modules don't have all the methods that we need for interpretation and compilation, so a new functor would be needed. We should re-eval this cleanup idea for v1.0.

Copy link
Contributor

@crusso crusso Jun 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's fine - also, it's not like were going to add more fixed width types in the foreseeable future...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Meta NIT: first-order -> first-class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Of course. Fixed comment above.


include Rep
let neg a = let res = Rep.neg a in check res
let abs a = let res = Rep.abs a in check res
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you've implemented all the binary interpretations in value.ml, I wonder if we should just implement the binary operators for AS - or is there much more work in the compiler to do this?
Again, as a separate PR, I'd think

Copy link
Contributor Author

@ggreif ggreif Jun 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be mostly straight-forward, as most arithmetic (8-32 width) can be performed in 64 bits and checked for overflow. pow and mul are trickier, but I know how to check for a fast-path.

I prefer to hand this work over to the IDL community, so they can integrate it, while (me) working on the backend operations in parallel.

@@ -411,7 +448,7 @@ and infer_exp'' env exp : T.typ =
| UnE (ot, op, exp1) ->
let t1 = infer_exp_promote env exp1 in
(* Special case for subtyping *)
let t = if t1 = T.Prim T.Nat then T.Prim T.Int else t1 in
let t = special_unop_typing t1 in
if not env.pre then begin
assert (!ot = Type.Pre);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be very tempted to changed the syntax type checker to annotate BinE and UnE with the partially evaluated interpretations of the operators, so we don't have to repeat dispatch every time we call an operator... Whadday think? Again, this would be as a separate PR...

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if I like code-in-the-data here. Would cause problems for pretty-printing, or serialization/deserialization. Also, smells a bit of premature optimization. Show me profiling data before you argue about performance :-)

Copy link
Contributor

@crusso crusso Jun 17, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nomeata Not sure I buy that. The pretty printer can ignore it, serializer and deserialized drop and recompute from the type stored in ot. And I really don't think I need a profiler to tell me that doing redundant work (think of interpreted arithmetic in a loop) is a bad idea.

Plus, it would be more true to the intention that operators are resolved statically, not at runtime. Indeed, weren't you arguing for this in the past? But true, none of that should be part of this PR....

interpret_exp env exp1 (fun v1 -> k (Operator.unop !ot op v1))
interpret_exp env exp1
(fun v1 ->
k (try Operator.unop !ot op v1 with Invalid_argument s -> trap exp.at "%s" s))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For that separate PR I alluded to, it's a shame that we repeatedly resolve operators at application. We could instead store the resolved operation once as an additional field in ot during type-checking and just directly use it during interpretation... In fact, if resolution took a regions as an argument you also partially evaluate the source location too, removing the need for the try with here...

Copy link
Contributor Author

@ggreif ggreif Jun 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume, by "resolution" you mean the worker Operator.unop ? Then yes, passing the srcloc to it makes a lot of sense.

let call_conv, f = V.as_func v1 in
check_call_conv exp1 call_conv;
check_call_conv_arg env exp v2 call_conv;
last_region := exp.at; (* in case the following throws *)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why your are object now, since we don't even have a stack of regions, you need to reset the region here. Yuck, but I still think it's ok for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EPARSE :-)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Err, I don't get the reference...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see why your are object now

My completion list for this:

  • "I see why you are objecting now"
  • "I see your objective now"

both don't really make sense to me in the context. Maybe you can explain?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I meant "I see why you are objecting now, ..."

Copy link
Collaborator

@nomeata nomeata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just looked at compile.ml

test/fail/outrange-nat64.as Show resolved Hide resolved
src/codegen/compile.ml Outdated Show resolved Hide resolved
src/codegen/compile.ml Outdated Show resolved Hide resolved
src/codegen/compile.ml Outdated Show resolved Hide resolved
src/codegen/compile.ml Outdated Show resolved Hide resolved
src/codegen/compile.ml Outdated Show resolved Hide resolved
src/codegen/compile.ml Outdated Show resolved Hide resolved
src/codegen/compile.ml Outdated Show resolved Hide resolved
src/codegen/compile.ml Show resolved Hide resolved
@ggreif
Copy link
Contributor Author

ggreif commented Jun 17, 2019

@crusso you requested changes, but I am not sure I can distill what exactly. I guess rewrite asc in Haskell was not it :-)

@crusso
Copy link
Contributor

crusso commented Jun 17, 2019

Well, I wanted you to remove the try with if possible (and wait for Joachim's review).

Copy link
Contributor

@crusso crusso left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm approving (pending Joachim's approval).

Copy link
Collaborator

@nomeata nomeata left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM as well

src/codegen/compile.ml Outdated Show resolved Hide resolved
I don't think allowing the non-canonical range in `Word*` patterns
is a good idea, so I have not enabled them in `desugar.ml`.
@@ -5132,6 +5132,48 @@ and compile_lit_pat env l =
| (NatLit _ | IntLit _) ->
compile_lit_as env SR.Vanilla l ^^
BigNum.compile_eq env
| Nat8Lit _ ->
snd (compile_lit env l) ^^
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see no point spelling out the SR for compile_lit_as all the time, instead I rely on what is done by both lines below implicitly. Also there is no guarantee that the latter operates on Vanilla (other than inspecting the implementation).

BoxedWord.unbox env ^^
snd (compile_lit env l) ^^
compile_eq env Type.(Prim Int64)
| Word8Lit _ ->
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Words were an earlier omission, filled in now.

snd (compile_lit env l) ^^
compile_eq env Type.(Prim Int16)
| Int32Lit _ ->
BoxedSmallWord.unbox env ^^
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the scrutinee is captured as SR.Vanilla we need to unbox it for *32 and *64 prim types.

src/codegen/compile.ml Outdated Show resolved Hide resolved
src/codegen/compile.ml Show resolved Hide resolved
and other review feedback
I am convinced now that they are not needed.

For the 8/16/32 bit types the OCaml `int32` provides the correct shift
semantics and conversion from `int`. `int` OTOH has enough bits to
represent the occurring ranges, negatives including.
@ggreif ggreif merged commit 1e1757a into master Jun 18, 2019
@ggreif ggreif deleted the gabor/ranged-int-nat branch June 18, 2019 18:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants