Skip to content

Commit c039eb6

Browse files
authored
Merge pull request #256 from fvutils/copilot/fix-indexerror-in-rangelist
Fix IndexError in variable bound propagators during constraint solving failures
2 parents 40aa1d6 + b62b958 commit c039eb6

File tree

4 files changed

+95
-20
lines changed

4 files changed

+95
-20
lines changed

src/vsc/model/variable_bound_max_propagator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ def propagate(self):
3434
must_propagate = False
3535
if i >= 0:
3636
# print("i: " + str(i) + " " + str(self.target.domain.range_l[i][0]))
37-
if range_l[i][1] > max_v:
37+
# Check if range_l is not empty before accessing
38+
if len(range_l) > i and range_l[i][1] > max_v:
3839
range_l[i][1] = max_v
3940
must_propagate = True
4041

@@ -43,6 +44,7 @@ def propagate(self):
4344
must_propagate = True
4445
# print("Removing domain element " + str(range_l[i+1]))
4546
self.target.domain.range_l = range_l[:i+1]
47+
range_l = self.target.domain.range_l # Update local reference
4648
else:
4749
# print("ran off the end")
4850
pass

src/vsc/model/variable_bound_min_propagator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,10 @@ def propagate(self):
3737
# Need to trim off full range elements
3838
must_propagate = True
3939
self.target.domain.range_l = range_l[i:]
40+
range_l = self.target.domain.range_l # Update local reference
4041

41-
if min_v > range_l[0][0]:
42+
# Check if range_l is not empty before accessing
43+
if len(range_l) > 0 and min_v > range_l[0][0]:
4244
range_l[0][0] = min_v
4345
must_propagate = True
4446
else:

src/vsc/types.py

Lines changed: 43 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,37 @@ def __init__(self, low, high):
281281
self.low = pop_expr()
282282
to_expr(high)
283283
self.high = pop_expr()
284+
285+
def _safe_to_expr_model(value):
286+
"""
287+
Safely convert a value to an expression model.
288+
289+
This function creates an expr object via to_expr() and extracts the
290+
expression model (.em) directly. It also attempts to pop from the global
291+
expression stack to maintain balance, but handles IndexError gracefully
292+
in case the stack is empty or corrupted (which can happen during constraint
293+
solving failures).
294+
295+
Note: to_expr() either:
296+
- Creates a new expr object (which pushes to stack in __init__), OR
297+
- Returns an existing expr object that's already on the stack
298+
In both cases, we need to pop to maintain stack balance.
299+
300+
Args:
301+
value: The value to convert to an expression model
302+
303+
Returns:
304+
The expression model object
305+
"""
306+
expr_obj = to_expr(value)
307+
model = expr_obj.em
308+
# Try to maintain stack balance, but don't fail if stack is corrupted
309+
try:
310+
pop_expr()
311+
except IndexError:
312+
# Stack was empty or corrupted, but we have the expression model
313+
pass
314+
return model
284315

285316
class rangelist(object):
286317

@@ -295,21 +326,18 @@ def __init__(self, *args):
295326
# This needs to be a two-element array
296327
if len(a) != 2:
297328
raise Exception("Range specified with " + str(len(a)) + " elements is invalid. Two elements required")
298-
to_expr(a[0])
299-
e0 = pop_expr()
300-
to_expr(a[1])
301-
e1 = pop_expr()
329+
# Use helper to safely get expression models
330+
e0 = _safe_to_expr_model(a[0])
331+
e1 = _safe_to_expr_model(a[1])
302332
self.range_l.add_range(ExprRangeModel(e0, e1))
303333
elif isinstance(a, rng):
304334
self.range_l.add_range(ExprRangeModel(a.low, a.high))
305335
elif isinstance(a, list):
306336
for ai in a:
307-
to_expr(ai)
308-
eai = pop_expr()
337+
eai = _safe_to_expr_model(ai)
309338
self.range_l.add_range(eai)
310339
else:
311-
to_expr(a)
312-
e = pop_expr()
340+
e = _safe_to_expr_model(a)
313341
self.range_l.add_range(e)
314342
# self.range_l.rl.reverse()
315343

@@ -327,26 +355,23 @@ def append(self, a):
327355
# This needs to be a two-element array
328356
if len(a) != 2:
329357
raise Exception("Range specified with " + str(len(a)) + " elements is invalid. Two elements required")
330-
to_expr(a[0])
331-
e0 = pop_expr()
332-
to_expr(a[1])
333-
e1 = pop_expr()
358+
# Use helper to safely get expression models
359+
e0 = _safe_to_expr_model(a[0])
360+
e1 = _safe_to_expr_model(a[1])
334361
self.range_l.add_range(ExprRangeModel(e0, e1))
335362
elif isinstance(a, rng):
336363
self.range_l.add_range(ExprRangeModel(a.low, a.high))
337364
elif isinstance(a, list):
338365
for ai in a:
339-
to_expr(ai)
340-
eai = pop_expr()
366+
eai = _safe_to_expr_model(ai)
341367
self.range_l.add_range(eai)
342368
else:
343-
to_expr(a)
344-
e = pop_expr()
369+
e = _safe_to_expr_model(a)
345370
self.range_l.add_range(e)
346371

347372
def __contains__(self, lhs):
348-
to_expr(lhs)
349-
return expr(ExprInModel(pop_expr(), self.range_l))
373+
lhs_model = _safe_to_expr_model(lhs)
374+
return expr(ExprInModel(lhs_model, self.range_l))
350375

351376
def __invert__(self):
352377
print("rangelist.__invert__")
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
'''
2+
Created on 2026-01-15
3+
4+
Test for rangelist tuple syntax bug with unsolvable constraints
5+
'''
6+
import unittest
7+
from vsc_test_case import VscTestCase
8+
import vsc
9+
10+
class TestConstraintRangelistBug(VscTestCase):
11+
12+
def test_rangelist_tuple_syntax_with_unsolvable_constraints(self):
13+
"""Test that tuple syntax in rangelist raises SolveFailure, not IndexError"""
14+
15+
@vsc.randobj
16+
class C():
17+
def __init__(self):
18+
self.a = vsc.rand_uint8_t()
19+
20+
@vsc.constraint
21+
def constr(self):
22+
self.a > 128
23+
24+
# Case 1: Tuple syntax - should throw SolveFailure (currently throws IndexError - BUG)
25+
c = C()
26+
with self.assertRaises(vsc.SolveFailure):
27+
with c.randomize_with() as it:
28+
it.a in vsc.rangelist((0, 50))
29+
30+
def test_rangelist_multiarg_syntax_with_unsolvable_constraints(self):
31+
"""Test that multi-arg syntax in rangelist correctly raises SolveFailure"""
32+
33+
@vsc.randobj
34+
class C():
35+
def __init__(self):
36+
self.a = vsc.rand_uint8_t()
37+
38+
@vsc.constraint
39+
def constr(self):
40+
self.a > 128
41+
42+
# Case 2: Multi-argument syntax - correctly throws SolveFailure
43+
c = C()
44+
with self.assertRaises(vsc.SolveFailure):
45+
with c.randomize_with() as it:
46+
it.a in vsc.rangelist(0, 50)

0 commit comments

Comments
 (0)