1
0
Fork 0
mirror of synced 2025-09-23 04:08:33 +00:00

Fix range check (#1309)

* fix uint range check

* update bit cache with correct bits

* suggestions

* js fmt

* clippy

* fix range check, add more tests

* update changelog

* fieldlt improvements

* fix tests
This commit is contained in:
Darko Macesic 2023-11-09 10:25:43 +01:00 committed by GitHub
parent c7e4e29ad0
commit 6c49edc075
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 1717 additions and 132 deletions

View file

@ -0,0 +1 @@
Improve safety in range checks

View file

@ -18,7 +18,7 @@ pub enum RuntimeError {
BranchIsolation,
ConstantLtBitness,
ConstantLtSum,
LtFinalSum,
LtSum,
LtSymetric,
Or,
Xor,
@ -83,7 +83,7 @@ impl fmt::Display for RuntimeError {
BranchIsolation => "Branch isolation failed",
ConstantLtBitness => "Bitness check failed in constant Lt check",
ConstantLtSum => "Sum check failed in constant Lt check",
LtFinalSum => "Sum check failed in final Lt check",
LtSum => "Sum check failed in Lt check",
LtSymetric => "Symetrical check failed in Lt check",
Or => "Or check failed",
Xor => "Xor check failed",

File diff suppressed because one or more lines are too long

View file

@ -841,7 +841,7 @@ impl<'ast, T: Field> Flattener<'ast, T> {
let lhs_id = self.define(lhs_flattened, statements_flattened);
let rhs_id = self.define(rhs_flattened, statements_flattened);
// shifted_sub := 2**safe_width + lhs - rhs
// shifted_sub := 2**bit_width + lhs - rhs
let shifted_sub = FlatExpression::add(
FlatExpression::value(T::from(2).pow(bit_width)),
FlatExpression::sub(
@ -857,7 +857,7 @@ impl<'ast, T: Field> Flattener<'ast, T> {
sub_width,
sub_width,
statements_flattened,
RuntimeError::IncompleteDynamicRange,
RuntimeError::Sum,
);
FlatExpression::sub(
@ -898,19 +898,42 @@ impl<'ast, T: Field> Flattener<'ast, T> {
.flatten_select_expression(statements_flattened, e)
.get_field_unchecked(),
BooleanExpression::FieldLt(e) => {
// Get the bit width to know the size of the binary decompositions for this Field
let bit_width = T::get_required_bits();
let safe_width = bit_width - 2; // dynamic comparison is not complete, it only applies to field elements whose difference is strictly smaller than 2**(bitwidth - 2)
let lhs_flattened = self.flatten_field_expression(statements_flattened, *e.left);
let rhs_flattened = self.flatten_field_expression(statements_flattened, *e.right);
self.lt_check(
statements_flattened,
lhs_flattened,
rhs_flattened,
safe_width,
)
match (lhs_flattened, rhs_flattened) {
(e, FlatExpression::Value(c)) => {
self.constant_lt_check(statements_flattened, e, c.value)
}
(FlatExpression::Value(c), e) => self.constant_lt_check(
statements_flattened,
FlatExpression::sub(T::max_value().into(), e),
T::max_value() - c.value,
),
(lhs, rhs) => {
// Get the bit width to know the size of the binary decompositions for this Field
let bit_width = T::get_required_bits();
let safe_width = bit_width - 2; // dynamic comparison is not complete, it only applies to field elements whose difference is strictly smaller than 2**(bitwidth - 2)
let safe_max = T::from(2).pow(safe_width);
// enforce that lhs and rhs are in the correct range
self.enforce_constant_lt_check(
statements_flattened,
lhs.clone(),
safe_max,
RuntimeError::IncompleteDynamicRange,
);
self.enforce_constant_lt_check(
statements_flattened,
rhs.clone(),
safe_max,
RuntimeError::IncompleteDynamicRange,
);
self.lt_check(statements_flattened, lhs, rhs, safe_width)
}
}
}
BooleanExpression::BoolEq(e) => {
// lhs and rhs are booleans, they flatten to 0 or 1
@ -1980,7 +2003,8 @@ impl<'ast, T: Field> Flattener<'ast, T> {
///
/// # Notes
/// * `from` and `to` must be smaller or equal to `T::get_required_bits()`, the bitwidth of the prime field
/// * the result is not checked to be in range. This is fine for `to < T::get_required_bits()`, but otherwise it is the caller's responsibility to add that check
/// * The result is not checked to be in range unless the bits of the expression were already decomposed with a higher bitwidth than `to`
/// * This is fine for `to < T::get_required_bits()`, but otherwise it is the caller's responsibility to add that check
fn get_bits_unchecked(
&mut self,
e: &FlatUExpression<T>,
@ -2015,19 +2039,30 @@ impl<'ast, T: Field> Flattener<'ast, T> {
let from = std::cmp::max(from, to);
let res = match self.bits_cache.entry(e.field.clone().unwrap()) {
Entry::Occupied(entry) => {
let res: Vec<_> = entry.get().clone();
// if we already know a decomposition, its number of elements has to be smaller or equal to `to`
let mut res: Vec<_> = entry.get().clone();
// only keep the last `to` values and return the sum of the others
let sum = res
.drain(0..res.len().saturating_sub(to))
.fold(FlatExpression::from(T::zero()), |acc, e| {
FlatExpression::add(acc, e)
});
// force the sum to be 0
statements_flattened.push_back(FlatStatement::condition(
FlatExpression::value(T::from(0)),
sum,
error,
));
// sanity check that we have at most `to` values
assert!(res.len() <= to);
// we then pad it with zeroes on the left (big endian) to return `to` bits
if res.len() == to {
res
} else {
(0..to - res.len())
.map(|_| FlatExpression::value(T::zero()))
.chain(res)
.collect()
}
// return the result left-padded to `to` values
std::iter::repeat(FlatExpression::value(T::zero()))
.take(to - res.len())
.chain(res.clone())
.collect()
}
Entry::Vacant(_) => {
let bits = (0..from).map(|_| self.use_sym()).collect::<Vec<_>>();
@ -2553,7 +2588,23 @@ impl<'ast, T: Field> Flattener<'ast, T> {
),
(lhs, rhs) => {
let bit_width = T::get_required_bits();
let safe_width = bit_width - 2; // dynamic comparison is not complete
let safe_width = bit_width - 2; // dynamic comparison is not complete, it only applies to field elements whose difference is strictly smaller than 2**(bitwidth - 2)
let safe_max = T::from(2).pow(safe_width);
self.enforce_constant_lt_check(
statements_flattened,
lhs.clone(),
safe_max,
RuntimeError::IncompleteDynamicRange,
);
self.enforce_constant_lt_check(
statements_flattened,
rhs.clone(),
safe_max,
RuntimeError::IncompleteDynamicRange,
);
let e = self.lt_check(statements_flattened, lhs, rhs, safe_width);
statements_flattened.push_back(FlatStatement::condition(
e,
@ -2583,7 +2634,23 @@ impl<'ast, T: Field> Flattener<'ast, T> {
),
(lhs, rhs) => {
let bit_width = T::get_required_bits();
let safe_width = bit_width - 2; // dynamic comparison is not complete
let safe_width = bit_width - 2; // dynamic comparison is not complete, it only applies to field elements whose difference is strictly smaller than 2**(bitwidth - 2)
let safe_max = T::from(2).pow(safe_width);
self.enforce_constant_lt_check(
statements_flattened,
lhs.clone(),
safe_max,
RuntimeError::IncompleteDynamicRange,
);
self.enforce_constant_lt_check(
statements_flattened,
rhs.clone(),
safe_max,
RuntimeError::IncompleteDynamicRange,
);
let e = self.le_check(statements_flattened, lhs, rhs, safe_width);
statements_flattened.push_back(FlatStatement::condition(
e,
@ -2593,10 +2660,49 @@ impl<'ast, T: Field> Flattener<'ast, T> {
}
}
}
BooleanExpression::UintLe(e) => {
BooleanExpression::UintLt(e) => {
let bitwidth = e.left.bitwidth as usize;
let lhs = self
.flatten_uint_expression(statements_flattened, *e.left)
.get_field_unchecked();
let rhs = self
.flatten_uint_expression(statements_flattened, *e.right)
.get_field_unchecked();
match (lhs, rhs) {
(e, FlatExpression::Value(c)) => self.enforce_constant_lt_check(
statements_flattened,
e,
c.value,
error.into(),
),
// c < e <=> 2^bw - 1 - e < 2^bw - 1 - c
(FlatExpression::Value(c), e) => {
let max = T::from(2u32).pow(bitwidth) - T::one();
self.enforce_constant_lt_check(
statements_flattened,
FlatExpression::sub(max.into(), e),
max - c.value,
error.into(),
)
}
(lhs, rhs) => {
let e = self.lt_check(statements_flattened, lhs, rhs, bitwidth);
statements_flattened.push_back(FlatStatement::condition(
e,
FlatExpression::value(T::one()),
error.into(),
));
}
}
}
BooleanExpression::UintLe(e) => {
let bitwidth = e.left.bitwidth as usize;
let lhs = self
.flatten_uint_expression(statements_flattened, *e.left)
.get_field_unchecked();
let rhs = self
.flatten_uint_expression(statements_flattened, *e.right)
.get_field_unchecked();
@ -2608,17 +2714,18 @@ impl<'ast, T: Field> Flattener<'ast, T> {
c.value,
error.into(),
),
// c <= e <=> p - 1 - e <= p - 1 - c
(FlatExpression::Value(c), e) => self.enforce_constant_le_check(
statements_flattened,
FlatExpression::sub(T::max_value().into(), e),
T::max_value() - c.value,
error.into(),
),
// c <= e <=> 2^bw - 1 - e <= 2^bw - 1 - c
(FlatExpression::Value(c), e) => {
let max = T::from(2u32).pow(bitwidth) - T::one();
self.enforce_constant_le_check(
statements_flattened,
FlatExpression::sub(max.into(), e),
max - c.value,
error.into(),
)
}
(lhs, rhs) => {
let bit_width = T::get_required_bits();
let safe_width = bit_width - 2; // dynamic comparison is not complete
let e = self.le_check(statements_flattened, lhs, rhs, safe_width);
let e = self.le_check(statements_flattened, lhs, rhs, bitwidth);
statements_flattened.push_back(FlatStatement::condition(
e,
FlatExpression::value(T::one()),

View file

@ -1,16 +0,0 @@
{
"entry_point": "./tests/tests/compare_min_to_max.zok",
"curves": ["Bn128", "Bls12_381", "Bls12_377", "Bw6_761"],
"tests": [
{
"input": {
"values": ["0"]
},
"output": {
"Ok": {
"value": false
}
}
}
]
}

View file

@ -1,10 +0,0 @@
from "field" import FIELD_MAX;
// /!\ should be called with a = 0
// as `|a - FIELD_MAX| < 2**(N-2)` the comparison should succeed
def main(field a) -> bool {
field p = FIELD_MAX + a;
// we added a = 0 to prevent the condition to be evaluated at compile time
return a < p;
}

View file

@ -0,0 +1,73 @@
{
"entry_point": "./tests/tests/range_check/assert_ge_big_constant.zok",
"max_constraint_count": 2,
"curves": ["Bn128"],
"tests": [
{
"input": {
"values": ["0"]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_ge_big_constant.zok",
"position": {
"line": 4,
"col": 5
}
}
}
}
}
}
},
{
"input": {
"values": [
"21888242871839275222246405745257275088548364400416034343698204186575808495614"
]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_ge_big_constant.zok",
"position": {
"line": 4,
"col": 5
}
}
}
}
}
}
},
{
"input": {
"values": [
"21888242871839275222246405745257275088548364400416034343698204186575808495615"
]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": [
"21888242871839275222246405745257275088548364400416034343698204186575808495616"
]
},
"output": {
"Ok": {
"value": []
}
}
}
]
}

View file

@ -0,0 +1,5 @@
from "field" import FIELD_MAX;
def main(field x) {
assert(x >= FIELD_MAX - 1);
}

View file

@ -1,5 +1,5 @@
{
"entry_point": "./tests/tests/range_check/assert_ge.zok",
"entry_point": "./tests/tests/range_check/assert_ge_constant.zok",
"max_constraint_count": 509,
"curves": ["Bn128"],
"tests": [
@ -12,7 +12,7 @@
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_ge.zok",
"file": "./tests/tests/range_check/assert_ge_constant.zok",
"position": {
"line": 2,
"col": 5
@ -32,7 +32,7 @@
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_ge.zok",
"file": "./tests/tests/range_check/assert_ge_constant.zok",
"position": {
"line": 2,
"col": 5

View file

@ -2,5 +2,4 @@ from "field" import FIELD_MAX;
def main(field x) {
assert(x > FIELD_MAX - 1);
return;
}

View file

@ -1,5 +1,5 @@
{
"entry_point": "./tests/tests/range_check/assert_gt.zok",
"entry_point": "./tests/tests/range_check/assert_gt_constant.zok",
"max_constraint_count": 508,
"curves": ["Bn128"],
"tests": [
@ -12,7 +12,7 @@
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_gt.zok",
"file": "./tests/tests/range_check/assert_gt_constant.zok",
"position": {
"line": 2,
"col": 5
@ -32,7 +32,7 @@
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_gt.zok",
"file": "./tests/tests/range_check/assert_gt_constant.zok",
"position": {
"line": 2,
"col": 5
@ -52,7 +52,7 @@
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_gt.zok",
"file": "./tests/tests/range_check/assert_gt_constant.zok",
"position": {
"line": 2,
"col": 5

View file

@ -1,11 +1,11 @@
{
"entry_point": "./tests/tests/range_check/assert_le.zok",
"max_constraint_count": 5,
"max_constraint_count": 763,
"curves": ["Bn128"],
"tests": [
{
"input": {
"values": ["1"]
"values": ["1", "2"]
},
"output": {
"Ok": {
@ -15,7 +15,7 @@
},
{
"input": {
"values": ["2"]
"values": ["2", "2"]
},
"output": {
"Ok": {
@ -25,7 +25,23 @@
},
{
"input": {
"values": ["3"]
"values": [
"1",
"7237005577332262213973186563042994240829374041602535252466099000494570602495"
]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": [
"7237005577332262213973186563042994240829374041602535252466099000494570602495",
"1"
]
},
"output": {
"Err": {
@ -45,20 +61,30 @@
},
{
"input": {
"values": ["15"]
"values": [
"1",
"7237005577332262213973186563042994240829374041602535252466099000494570602496"
]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_le.zok",
"position": {
"line": 2,
"col": 5
}
}
}
"error": "IncompleteDynamicRange"
}
}
}
},
{
"input": {
"values": [
"7237005577332262213973186563042994240829374041602535252466099000494570602496",
"1"
]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": "IncompleteDynamicRange"
}
}
}

View file

@ -1,4 +1,3 @@
def main(field x) {
assert(x <= 2);
return;
def main(field a, field b) {
assert(a <= b);
}

View file

@ -0,0 +1,63 @@
{
"entry_point": "./tests/tests/range_check/assert_le_big_constant.zok",
"max_constraint_count": 508,
"curves": ["Bn128"],
"tests": [
{
"input": {
"values": ["0"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": [
"21888242871839275222246405745257275088548364400416034343698204186575808495614"
]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": [
"21888242871839275222246405745257275088548364400416034343698204186575808495615"
]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": [
"21888242871839275222246405745257275088548364400416034343698204186575808495616"
]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_le_big_constant.zok",
"position": {
"line": 4,
"col": 5
}
}
}
}
}
}
}
]
}

View file

@ -0,0 +1,5 @@
from "field" import FIELD_MAX;
def main(field x) {
assert(x <= FIELD_MAX - 1);
}

View file

@ -0,0 +1,67 @@
{
"entry_point": "./tests/tests/range_check/assert_le_constant.zok",
"max_constraint_count": 5,
"curves": ["Bn128"],
"tests": [
{
"input": {
"values": ["1"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["2"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["3"]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_le_constant.zok",
"position": {
"line": 2,
"col": 5
}
}
}
}
}
}
},
{
"input": {
"values": ["15"]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_le_constant.zok",
"position": {
"line": 2,
"col": 5
}
}
}
}
}
}
}
]
}

View file

@ -0,0 +1,3 @@
def main(field x) {
assert(x <= 2);
}

View file

@ -0,0 +1,47 @@
{
"entry_point": "./tests/tests/range_check/assert_le_u8.zok",
"max_constraint_count": 31,
"curves": ["Bn128"],
"tests": [
{
"input": {
"values": ["0x01", "0x02"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["0x02", "0x02"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["0x04", "0x02"]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_le_u8.zok",
"position": {
"line": 2,
"col": 5
}
}
}
}
}
}
}
]
}

View file

@ -0,0 +1,3 @@
def main(u8 a, u8 b) {
assert(a <= b);
}

View file

@ -0,0 +1,57 @@
{
"entry_point": "./tests/tests/range_check/assert_le_u8_constant.zok",
"max_constraint_count": 12,
"curves": ["Bn128"],
"tests": [
{
"input": {
"values": ["0x00"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["0x01"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["0x02"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["0x0f"]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_le_u8_constant.zok",
"position": {
"line": 2,
"col": 5
}
}
}
}
}
}
}
]
}

View file

@ -0,0 +1,3 @@
def main(u8 x) {
assert(x <= 2);
}

View file

@ -1,11 +1,11 @@
{
"entry_point": "./tests/tests/range_check/assert_lt.zok",
"max_constraint_count": 4,
"max_constraint_count": 764,
"curves": ["Bn128"],
"tests": [
{
"input": {
"values": ["0"]
"values": ["1", "2"]
},
"output": {
"Ok": {
@ -15,7 +15,10 @@
},
{
"input": {
"values": ["1"]
"values": [
"1",
"7237005577332262213973186563042994240829374041602535252466099000494570602495"
]
},
"output": {
"Ok": {
@ -25,7 +28,10 @@
},
{
"input": {
"values": ["2"]
"values": [
"7237005577332262213973186563042994240829374041602535252466099000494570602495",
"1"
]
},
"output": {
"Err": {
@ -45,20 +51,30 @@
},
{
"input": {
"values": ["15"]
"values": [
"1",
"7237005577332262213973186563042994240829374041602535252466099000494570602496"
]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_lt.zok",
"position": {
"line": 2,
"col": 5
}
}
}
"error": "IncompleteDynamicRange"
}
}
}
},
{
"input": {
"values": [
"7237005577332262213973186563042994240829374041602535252466099000494570602496",
"1"
]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": "IncompleteDynamicRange"
}
}
}

View file

@ -1,4 +1,3 @@
def main(field x) {
assert(x < 2);
return;
def main(field a, field b) {
assert(a < b);
}

View file

@ -2,5 +2,4 @@ from "field" import FIELD_MAX;
def main(field x) {
assert(x < FIELD_MAX - 1);
return;
}

View file

@ -0,0 +1,67 @@
{
"entry_point": "./tests/tests/range_check/assert_lt_constant.zok",
"max_constraint_count": 4,
"curves": ["Bn128"],
"tests": [
{
"input": {
"values": ["0"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["1"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["2"]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_lt_constant.zok",
"position": {
"line": 2,
"col": 5
}
}
}
}
}
}
},
{
"input": {
"values": ["15"]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_lt_constant.zok",
"position": {
"line": 2,
"col": 5
}
}
}
}
}
}
}
]
}

View file

@ -0,0 +1,3 @@
def main(field x) {
assert(x < 2);
}

View file

@ -1,11 +1,11 @@
{
"entry_point": "./tests/tests/range_check/assert_lt_u8.zok",
"max_constraint_count": 9,
"max_constraint_count": 29,
"curves": ["Bn128"],
"tests": [
{
"input": {
"values": ["0x00"]
"values": ["0x01", "0x02"]
},
"output": {
"Ok": {
@ -15,17 +15,7 @@
},
{
"input": {
"values": ["0x01"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["0x02"]
"values": ["0x02", "0x02"]
},
"output": {
"Err": {
@ -45,7 +35,7 @@
},
{
"input": {
"values": ["0x0f"]
"values": ["0x04", "0x02"]
},
"output": {
"Err": {

View file

@ -1,4 +1,3 @@
def main(field x) {
assert(x < 2);
return;
def main(u8 a, u8 b) {
assert(a < b);
}

View file

@ -0,0 +1,67 @@
{
"entry_point": "./tests/tests/range_check/assert_lt_u8_constant.zok",
"max_constraint_count": 10,
"curves": ["Bn128"],
"tests": [
{
"input": {
"values": ["0x00"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["0x01"]
},
"output": {
"Ok": {
"value": []
}
}
},
{
"input": {
"values": ["0x02"]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_lt_u8_constant.zok",
"position": {
"line": 2,
"col": 5
}
}
}
}
}
}
},
{
"input": {
"values": ["0x0f"]
},
"output": {
"Err": {
"UnsatisfiedConstraint": {
"error": {
"SourceAssertion": {
"file": "./tests/tests/range_check/assert_lt_u8_constant.zok",
"position": {
"line": 2,
"col": 5
}
}
}
}
}
}
}
]
}

View file

@ -0,0 +1,3 @@
def main(u8 x) {
assert(x < 2);
}