Unused argument elimination discards required field
landonf opened this issue · 1 comments
The following fails with an assert in krml
(see below)
module CB = LowStar.ConstBuffer
module U8 = FStar.UInt8
module U32 = FStar.UInt32
inline_for_extraction
type buftype =
| CONST
inline_for_extraction
let buffer_t (ty:buftype) (a:Type0) =
match ty with
| CONST -> CB.const_buffer a
inline_for_extraction
let length (#t:buftype) (#a:Type0) (b:buffer_t t a): GTot nat =
match t with
| CONST -> CB.length (b <: buffer_t CONST a)
inline_for_extraction
noeq type slice_t (t:buftype) (a:Type0) = {
len: U32.t;
bytes: (b:buffer_t t a { length b == U32.v len });
}
let example (s:slice_t CONST U8.t) = s.bytes
Fails with:
> fstar --odir . --codegen Kremlin BufferSlice.fst && krml out.krml
...
✔ [Monomorphization] ⏱️ 31ms
✔ [Inlining] ⏱️ 3ms
Fatal error: exception File "src/DataTypes.ml", line 508, characters 11-17: Assertion failed
The assert is in remove_unit_fields
:
method private default_value = function
| TUnit -> EUnit
| TAny -> EAny
| _ -> assert false
Not a Contribution.
Hi Landon,
Unfortunately I don't think you can achieve this as-is. Let me try to explain a little bit. After processing all of the inline_for_extraction's (let's ignore refinements which are irrelevant and get erased anyhow), you're getting this:
type slice t a = {
len: u32;
bytes: (match t with CONST -> const_buffer a);
}
let example (s: slice CONST u8) = s.bytes
This normalization is roughly the first step that happens in the extraction pipeline. The next step is erasing types that would prevent the program from type-checking with, roughly, ML-like polymorphism (this is a Letouzey-style extraction). This means in particular that the bytes
field, having an indexed type (a match), gets erased to unit, because match ...
is not a valid type, neither in Low* (or in ML for that matter). When a meaningful type like this gets erased to unit, the program falls outside of the Low* subset and all bets are off. (Granted, it could be better surfaced to the user than having me write an explanation on GitHub.)
Then, KreMLin thinks that this field is a unit and eliminates it, causing problems further down the pipeline.
I don't know much about your use-case, so it's hard to speculate, but let me try to offer a few suggestions.
General solution
There is a workaround that I established recently and documented here: https://github.com/project-everest/hacl-star/blob/master/code/streaming/Hacl.Streaming.Functor.fst#L29 -- well, sort of documented. (I had the same problem as you just a few months ago.)
Let me explain: you can introduce a spurious type parameter whose sole purpose is to superficially make your data type definition look like it's parameterized over a type. Here's your example, fixed, along with a demo of how to write buftype-polymorphic functions.
module Test
module CB = LowStar.ConstBuffer
module U8 = FStar.UInt8
module U32 = FStar.UInt32
inline_for_extraction
type buftype =
| CONST
inline_for_extraction
let buffer_t (ty:buftype) (a:Type0) =
match ty with
| CONST -> CB.const_buffer a
inline_for_extraction
let length (#t:buftype) (#a:Type0) (b:buffer_t t a): GTot nat =
match t with
| CONST -> CB.length (b <: buffer_t CONST a)
// This extracts in ML as:
// type 'b slice_t = { len: U32.t; bytes: 'b }
// which then triggers whole-program monomorphization over type parameter 'b.
inline_for_extraction
noeq type slice_t (t:Ghost.erased buftype) (a:Type0) (b: Type0 { b == buffer_t t a }) = {
len: U32.t;
bytes: (buf:b { length (buf <: buffer_t t a) == U32.v len });
}
// An abbreviation for convenience.
inline_for_extraction noextract
let slice_t' (t: Ghost.erased buftype) (a: Type0) =
slice_t t a (buffer_t t a)
// No particular precautions are required to use slice_t' provided the t is a
// constant value that can trigger reduction (you get a monomorphic function).
let example (s:slice_t' CONST U8.t) = s.bytes
// If you want to write a slice-polymorphic function, you need to enforce
// ML-style prenex polymorphism via a type parameter that appears *first*.
let length' (#t: Ghost.erased buftype) (#a:Type0) (#b: Type0 { b == buffer_t t a }) (s: slice_t t a b) =
s.len
module B = LowStar.Buffer
let test (): FStar.HyperStack.ST.St UInt32.t =
let b = B.malloc FStar.HyperStack.root 0uy 1ul in
let b = CB.of_buffer b in
let s: slice_t' CONST UInt8.t = { bytes = b; len = 1ul } in
length' s
Note that you will get a monomorphization of the type slice_t
for each flavor of buffer you use in your program.
// Specialized definition of slice, for b == CB.const_buffer UInt8.t, obtained via KreMLin whole-program monomorphization
typedef struct Test_slice_t__const_uint8_t__s
{
uint32_t len;
const uint8_t *bytes;
}
Test_slice_t__const_uint8_t_;
// Specialized definition of example, obtained via F* reduction since the arguments to the type are constant
const uint8_t *Test_example(Test_slice_t__const_uint8_t_ s);
// No polymorphism in C, so the polymorphic function length is not compiled, but for each use-site, a monomorphic instance is generated, in this case, from the use of `length'` in test (). Also note how the judicious use of `erased` in the F* code makes sure the signature of Test_length' is clean and doesn't mention any buftype parameter. (It would without the erased, which would be a shame.)
uint32_t Test_length_(uint32_t s);
Ad-hoc solution
You can probably replace the record type with a pair to achieve the same effect.
If this helps, don't hesitate to continue the discussion on zulip (https://fstar.zulipchat.com/#narrow/stream/184683-questions-and.20help) so that others can benefit from it, or feel free to submit a PR to the Low* tutorial to include this trick in the tips & tricks section.
Cheers,
Jonathan