Repository navigation
C API: Add constructors for classical variables, stretches and values - #17396
Noam Hirshberg (noam-hb) wants to merge 1 commit into
Conversation
8cdfafb to
42a604d
Compare
|
One or more of the following people are relevant to this code:
|
Add C API functions for creating the leaf nodes of classical expressions. This is part 1 for issue #16667 for classical expression constructors for C. = Added functions: - QkVar: `qk_var_new`, `qk_var_free`. - QkStretch: `qk_stretch_new`, `qk_stretch_free`. - QkValue: - `qk_value_new_duration` - `qk_value_new_float` - `qk_value_new_uint` - `qk_value_new_biguint` - `qk_value_free` Unlike the proposal in the issue, `qk_value_new_[big]uint` do take the bit-width too as a parameter. They default to "full width" of the uint or buffer given when you pass `bit_width=0` -- this is unlike Python, but more sensible in C, IMO. = Error Handling: We return `NULL` from the added functions in these error cases: - `qk_var_new`/`qk_stretch_new` name parameter is `NULL` - `qk_var_new` type parameter is `uint` with bit-width zero - `qk_value_new_uint`/`qk_value_new_biguint` integer does not fit in the given `bit_width` Since these functions return a pointer anyway, we can return `NULL` for these cases instead of panicking. This lets the caller check and recover, if they so choose. Still, there are tradeoffs in doing this instead of panicking -- panicking would be much easier to debug in some cases.
42a604d to
8ebf9c6
Compare
David F. Cohen (davidfcohen)
left a comment
There was a problem hiding this comment.
Noam, this API is coming together nicely. I have a few comments.
| #[unsafe(no_mangle)] | ||
| pub extern "C" fn qk_value_new_uint(uint: u64, bit_width: u32) -> *mut Value { | ||
| let bit_width = match bit_width { | ||
| 0 => 64, |
There was a problem hiding this comment.
If the caller writes qk_value_new_uint(val, 0), the real width is obscured when reading the caller code. Consider returning NULL instead.
There was a problem hiding this comment.
I did consider making bit_width=0 an error case. That is also a sensible option to go with, but not what I did.
What is implemented here is interpreting bit_width=0 as "use the full width of the input", which is 64 bits for a uint. The user can still decide to be explicit and pass the bit_width they want every time. The zero case is simply for convenience here.
I think this is fine as is, but if you see more concrete problems with this behavior, do tell.
There was a problem hiding this comment.
I'm also fine leaving it as is. Another path you could take is:
qk_value_new_uint(uint: u64)
qk_value_new_uint_width(uint: u64, width: 0)
The second function is your current one. Then qk_value_new_uint could call it. If you're keeping things as is, feel free to resolve this thread.
| // SAFETY: Per documentation, the `QkBigUint` is valid. | ||
| let biguint_slice = unsafe { biguint.as_slice() }; | ||
| // NOTE: there is no constructor from `&[u64]`? weird. | ||
| let biguint = BigUint::new( |
There was a problem hiding this comment.
It would be nice to have a helper function that builds a BigUint from CBigUint.
There was a problem hiding this comment.
It would be nice, yeah. It would have to be unsafe though. I added CBigUint::as_slice (unsafe), with the intention to use it like so:
BigUint::from_slice(unsafe { biguint.as_slice() })But it turns out BigUint::from_slice takes &[u32] and there is no similar method that takes &[u64], although limbs on 64bit machines are u64, so go figure.
Anyway, I tried implementing it and using it right now by extracting it from this code, but it didn't work out exactly how I wanted: because I use biguint_slice: &[u64] just afterwards to get the number of limbs, so I need it as an intermediate step. You can actually do biguint.iter_u64_digits().len() on a BigUint and it's O(1) because that iter method returns an ExactSizeIterator. Writing iter().len() for a calculation that should be O(1) looks so much like a red flag though that I don't even want to write it.
Implementing it like so actually changes the inferred bit_width metadata (it is inferred only when the caller passes bit_width=0) because creating a BigUint will truncate trailing zero limbs. Just the fact that this is relevant at all might be a good argument for maybe just making bit_width=0 an error case instead.
I will think through this a bit more before deciding what I do here.
| .flat_map(|&d| [d as u32, (d >> 32) as u32]) | ||
| .collect(), | ||
| ); | ||
| let bit_width: u32 = match bit_width { |
There was a problem hiding this comment.
Are we obscuring behavior by taking a "best-effort" approach when bit_width is 0?
There was a problem hiding this comment.
The intention is not so much doing "best-effort" but that bit_width=0 implies "use full width of the buffer" without the caller having to calculate it or remember it.
The question here is whether this is a common and sensible thing that the caller wants. I intuit that basically yes, but I could be wrong on this.
Also see relevant parts of my comments above that also touch on this.
There was a problem hiding this comment.
See previous comment. I trust your judgement on this.
| /* | ||
| * Test constructing standalone variables and stretches. | ||
| */ | ||
| static int test_var_stretch_new(void) { |
There was a problem hiding this comment.
You should break up this test. The error cases are independently testable. Also, this function name suggests that we're only testing QkStretch, but we're testing QkVar too.
- stretch success
- stretch with null name
- var success
- var with null name
- var with uint width 0
Try breaking up the other tests too.
There was a problem hiding this comment.
I think I will split it to one function for QkVar and one for QkStretch, covering all the cases per each.
There was a problem hiding this comment.
I'm happy with this middle ground. I think it would be convenient for both name = NULL and width = 0 to be tested independently, but I understand why combining them is convenient.
Add C API functions for creating the leaf nodes of classical expressions. This is part 1 for issue #16667 for classical expression constructors for C.
Added functions:
qk_var_new,qk_var_free.qk_stretch_new,qk_stretch_free.qk_value_new_durationqk_value_new_floatqk_value_new_uintqk_value_new_biguintqk_value_freeUnlike the proposal in the issue,
qk_value_new_[big]uintdo take the bit-width too as a parameter. They default to "full width" of the uint or buffer given when you passbit_width=0-- this is unlike Python, but more sensible in C, IMO.AI/LLM disclosure