Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/vsc/model/variable_bound_max_propagator.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def propagate(self):
must_propagate = False
if i >= 0:
# print("i: " + str(i) + " " + str(self.target.domain.range_l[i][0]))
if range_l[i][1] > max_v:
# Check if range_l is not empty before accessing
if len(range_l) > i and range_l[i][1] > max_v:
range_l[i][1] = max_v
must_propagate = True

Expand All @@ -43,6 +44,7 @@ def propagate(self):
must_propagate = True
# print("Removing domain element " + str(range_l[i+1]))
self.target.domain.range_l = range_l[:i+1]
range_l = self.target.domain.range_l # Update local reference
else:
# print("ran off the end")
pass
Expand Down
4 changes: 3 additions & 1 deletion src/vsc/model/variable_bound_min_propagator.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,10 @@ def propagate(self):
# Need to trim off full range elements
must_propagate = True
self.target.domain.range_l = range_l[i:]
range_l = self.target.domain.range_l # Update local reference

if min_v > range_l[0][0]:
# Check if range_l is not empty before accessing
if len(range_l) > 0 and min_v > range_l[0][0]:
range_l[0][0] = min_v
must_propagate = True
else:
Expand Down
61 changes: 43 additions & 18 deletions src/vsc/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,37 @@ def __init__(self, low, high):
self.low = pop_expr()
to_expr(high)
self.high = pop_expr()

def _safe_to_expr_model(value):
"""
Safely convert a value to an expression model.

This function creates an expr object via to_expr() and extracts the
expression model (.em) directly. It also attempts to pop from the global
expression stack to maintain balance, but handles IndexError gracefully
in case the stack is empty or corrupted (which can happen during constraint
solving failures).

Note: to_expr() either:
- Creates a new expr object (which pushes to stack in __init__), OR
- Returns an existing expr object that's already on the stack
In both cases, we need to pop to maintain stack balance.

Args:
value: The value to convert to an expression model

Returns:
The expression model object
"""
expr_obj = to_expr(value)
model = expr_obj.em
# Try to maintain stack balance, but don't fail if stack is corrupted
try:
pop_expr()
except IndexError:
# Stack was empty or corrupted, but we have the expression model
pass
return model

class rangelist(object):

Expand All @@ -295,21 +326,18 @@ def __init__(self, *args):
# This needs to be a two-element array
if len(a) != 2:
raise Exception("Range specified with " + str(len(a)) + " elements is invalid. Two elements required")
to_expr(a[0])
e0 = pop_expr()
to_expr(a[1])
e1 = pop_expr()
# Use helper to safely get expression models
e0 = _safe_to_expr_model(a[0])
e1 = _safe_to_expr_model(a[1])
self.range_l.add_range(ExprRangeModel(e0, e1))
elif isinstance(a, rng):
self.range_l.add_range(ExprRangeModel(a.low, a.high))
elif isinstance(a, list):
for ai in a:
to_expr(ai)
eai = pop_expr()
eai = _safe_to_expr_model(ai)
self.range_l.add_range(eai)
else:
to_expr(a)
e = pop_expr()
e = _safe_to_expr_model(a)
self.range_l.add_range(e)
# self.range_l.rl.reverse()

Expand All @@ -327,26 +355,23 @@ def append(self, a):
# This needs to be a two-element array
if len(a) != 2:
raise Exception("Range specified with " + str(len(a)) + " elements is invalid. Two elements required")
to_expr(a[0])
e0 = pop_expr()
to_expr(a[1])
e1 = pop_expr()
# Use helper to safely get expression models
e0 = _safe_to_expr_model(a[0])
e1 = _safe_to_expr_model(a[1])
self.range_l.add_range(ExprRangeModel(e0, e1))
elif isinstance(a, rng):
self.range_l.add_range(ExprRangeModel(a.low, a.high))
elif isinstance(a, list):
for ai in a:
to_expr(ai)
eai = pop_expr()
eai = _safe_to_expr_model(ai)
self.range_l.add_range(eai)
else:
to_expr(a)
e = pop_expr()
e = _safe_to_expr_model(a)
self.range_l.add_range(e)

def __contains__(self, lhs):
to_expr(lhs)
return expr(ExprInModel(pop_expr(), self.range_l))
lhs_model = _safe_to_expr_model(lhs)
return expr(ExprInModel(lhs_model, self.range_l))

def __invert__(self):
print("rangelist.__invert__")
Expand Down
46 changes: 46 additions & 0 deletions ve/unit/test_constraint_rangelist_bug.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
'''
Created on 2026-01-15

Test for rangelist tuple syntax bug with unsolvable constraints
'''
import unittest
from vsc_test_case import VscTestCase
import vsc

class TestConstraintRangelistBug(VscTestCase):

def test_rangelist_tuple_syntax_with_unsolvable_constraints(self):
"""Test that tuple syntax in rangelist raises SolveFailure, not IndexError"""

@vsc.randobj
class C():
def __init__(self):
self.a = vsc.rand_uint8_t()

@vsc.constraint
def constr(self):
self.a > 128

# Case 1: Tuple syntax - should throw SolveFailure (currently throws IndexError - BUG)
c = C()
with self.assertRaises(vsc.SolveFailure):
with c.randomize_with() as it:
it.a in vsc.rangelist((0, 50))

def test_rangelist_multiarg_syntax_with_unsolvable_constraints(self):
"""Test that multi-arg syntax in rangelist correctly raises SolveFailure"""

@vsc.randobj
class C():
def __init__(self):
self.a = vsc.rand_uint8_t()

@vsc.constraint
def constr(self):
self.a > 128

# Case 2: Multi-argument syntax - correctly throws SolveFailure
c = C()
with self.assertRaises(vsc.SolveFailure):
with c.randomize_with() as it:
it.a in vsc.rangelist(0, 50)