Skip to content

Commit 26afeb2

Browse files
authored
feat(think): Add list indexing error handling (#59)
This commit adds comprehensive error handling for list indexing operations in the Think language interpreter, along with extensive test coverage. Key changes: - Add proactive validation of list indices before access - Validate numeric types and whole numbers for indices - Add explicit bounds checking for list access - Improve error messages for invalid indexing operations New tests: - test_list_indexing_errors: Tests basic indexing error cases - Out of bounds access (positive and negative indices) - Non-integer indices - Invalid types for indices - test_nested_list_indexing_errors: Tests nested indexing errors - Accessing index of non-list values - Out of bounds on nested lists The error handling now catches issues before Python's built-in errors, providing clearer error messages specific to the Think language.
1 parent dca105e commit 26afeb2

File tree

2 files changed

+200
-14
lines changed

2 files changed

+200
-14
lines changed

tests/test_integration.py

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import io
33
import sys
44
from think.interpreter import ThinkInterpreter
5+
from think.errors import ThinkRuntimeError
56

67
@pytest.fixture
78
def capture_output():
@@ -145,6 +146,137 @@ def test_list_operations(self, interpreter, parser, capture_output):
145146
interpreter.execute(ast)
146147
assert interpreter.state['items'][0] == 0
147148
assert interpreter.state['items'][2] == 2
149+
150+
def test_list_indexing(self, interpreter, parser, capture_output):
151+
"""Test comprehensive list indexing operations."""
152+
code = '''objective "Test list indexing"
153+
task "ListIndexing":
154+
step "Setup":
155+
numbers = [10, 20, 30, 40, 50]
156+
157+
first = numbers[0]
158+
last = numbers[4]
159+
160+
last_item = numbers[-1]
161+
second_to_last = numbers[-2]
162+
163+
idx = 2
164+
middle = numbers[idx]
165+
166+
expr_idx = numbers[1 + 1]
167+
168+
matrix = [[1, 2, 3], [4, 5, 6]]
169+
nested_val = matrix[1][2]
170+
171+
calc_idx = 10 / 2 - 1
172+
computed = numbers[calc_idx]
173+
174+
run "ListIndexing"'''
175+
176+
ast = parser.parse(code)
177+
interpreter.execute(ast)
178+
179+
# Verify basic positive indexing
180+
assert interpreter.state['first'] == 10, "First element incorrect"
181+
assert interpreter.state['last'] == 50, "Last element incorrect"
182+
183+
# Verify negative indexing
184+
assert interpreter.state['last_item'] == 50, "Negative indexing failed"
185+
assert interpreter.state['second_to_last'] == 40, "Negative indexing failed"
186+
187+
# Verify variable as index
188+
assert interpreter.state['middle'] == 30, "Variable index failed"
189+
190+
# Verify expression as index
191+
assert interpreter.state['expr_idx'] == 30, "Expression index failed"
192+
193+
# Verify nested indexing
194+
assert interpreter.state['nested_val'] == 6, "Nested indexing failed"
195+
196+
# Verify computed index
197+
assert interpreter.state['computed'] == 50, "Computed index failed"
198+
199+
def test_list_indexing_errors(self, interpreter, parser, capture_output):
200+
"""Test error cases for list indexing."""
201+
# Test index out of bounds (positive)
202+
code = '''objective "Test list index errors"
203+
task "ListErrors":
204+
step "OutOfBounds":
205+
numbers = [1, 2, 3]
206+
invalid = numbers[5]
207+
run "ListErrors"'''
208+
209+
with pytest.raises(ThinkRuntimeError) as exc_info:
210+
ast = parser.parse(code)
211+
interpreter.execute(ast)
212+
assert "Invalid index/key" in str(exc_info.value)
213+
214+
# Test index out of bounds (negative)
215+
code = '''objective "Test negative index errors"
216+
task "ListErrors":
217+
step "NegativeOutOfBounds":
218+
numbers = [1, 2, 3]
219+
invalid = numbers[-4]
220+
run "ListErrors"'''
221+
222+
with pytest.raises(ThinkRuntimeError) as exc_info:
223+
ast = parser.parse(code)
224+
interpreter.execute(ast)
225+
assert "Invalid index/key" in str(exc_info.value)
226+
227+
# Test non-integer index
228+
code = '''objective "Test non-integer index"
229+
task "ListErrors":
230+
step "NonInteger":
231+
numbers = [1, 2, 3]
232+
invalid = numbers[1.5]
233+
run "ListErrors"'''
234+
235+
with pytest.raises(ThinkRuntimeError) as exc_info:
236+
ast = parser.parse(code)
237+
interpreter.execute(ast)
238+
assert "Invalid index/key" in str(exc_info.value)
239+
240+
# Test invalid type for index
241+
code = '''objective "Test invalid index type"
242+
task "ListErrors":
243+
step "InvalidType":
244+
numbers = [1, 2, 3]
245+
invalid = numbers["one"]
246+
run "ListErrors"'''
247+
248+
with pytest.raises(ThinkRuntimeError) as exc_info:
249+
ast = parser.parse(code)
250+
interpreter.execute(ast)
251+
assert "Invalid index/key" in str(exc_info.value)
252+
253+
def test_nested_list_indexing_errors(self, interpreter, parser, capture_output):
254+
"""Test error cases for nested list indexing."""
255+
# Test accessing index of non-list
256+
code = '''objective "Test invalid nested indexing"
257+
task "NestedErrors":
258+
step "NonList":
259+
numbers = [1, [2, 3], 4]
260+
invalid = numbers[0][1]
261+
run "NestedErrors"'''
262+
263+
with pytest.raises(ThinkRuntimeError) as exc_info:
264+
ast = parser.parse(code)
265+
interpreter.execute(ast)
266+
assert "Cannot index into type" in str(exc_info.value)
267+
268+
# Test out of bounds on nested list
269+
code = '''objective "Test nested out of bounds"
270+
task "NestedErrors":
271+
step "OutOfBounds":
272+
matrix = [[1, 2], [3, 4]]
273+
invalid = matrix[1][5]
274+
run "NestedErrors"'''
275+
276+
with pytest.raises(ThinkRuntimeError) as exc_info:
277+
ast = parser.parse(code)
278+
interpreter.execute(ast)
279+
assert "Invalid index" in str(exc_info.value)
148280

149281

150282
class TestFunctions:

think/interpreter.py

Lines changed: 68 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,7 @@ def execute_statement(self, statement):
322322
elif stmt_type == 'decide':
323323
return self.execute_decide(statement)
324324

325+
325326
def evaluate_expression(self, expr):
326327
"""Evaluate an expression and return its value"""
327328
# Handle direct values
@@ -352,36 +353,87 @@ def evaluate_expression(self, expr):
352353
container = self.evaluate_expression(expr['container'])
353354
key = self.evaluate_expression(expr['key'])
354355

355-
# Convert string literal to string if it's being used as a key
356-
if isinstance(key, dict) and key.get('type') == 'string_literal':
357-
key = key['value']
358-
359-
if isinstance(container, (dict, list)):
356+
# Handle list indexing
357+
if isinstance(container, list):
360358
try:
361-
if isinstance(container, list):
359+
# First validate that key can be used as an index
360+
if not isinstance(key, (int, float)) or isinstance(key, bool):
361+
raise ThinkRuntimeError(
362+
message=f"Invalid index/key: List indices must be integers, got {type(key).__name__}",
363+
task=self.current_task,
364+
step=self.current_step,
365+
variables={
366+
"attempted_key": key,
367+
"key_type": type(key).__name__
368+
}
369+
)
370+
371+
# Convert float to int if it's a whole number
372+
if isinstance(key, float):
373+
if not key.is_integer():
374+
raise ThinkRuntimeError(
375+
message=f"Invalid index/key: List indices must be whole numbers, got {key}",
376+
task=self.current_task,
377+
step=self.current_step,
378+
variables={
379+
"attempted_key": key
380+
}
381+
)
362382
key = int(key)
383+
384+
# Check bounds before accessing
385+
if key >= len(container) or key < -len(container):
386+
raise ThinkRuntimeError(
387+
message=f"Invalid index/key: {key} is out of range for list of length {len(container)}",
388+
task=self.current_task,
389+
step=self.current_step,
390+
variables={
391+
"attempted_key": key,
392+
"list_length": len(container),
393+
"valid_range": f"-{len(container)} to {len(container)-1}"
394+
}
395+
)
396+
363397
return container[key]
364-
except (KeyError, IndexError, ValueError) as e:
398+
399+
except (TypeError, ValueError) as e:
365400
raise ThinkRuntimeError(
366-
message=f"Invalid index/key: {key} for container {container}",
401+
message=f"Invalid index/key: {key}",
367402
task=self.current_task,
368403
step=self.current_step,
369404
variables={
370-
"container_type": type(container).__name__,
371-
"container_value": container,
372405
"attempted_key": key,
373-
"valid_keys": list(container.keys()) if isinstance(container, dict) else f"0-{len(container)-1}"
406+
"error": str(e)
374407
}
375408
)
409+
410+
# Handle dictionary indexing
411+
elif isinstance(container, dict):
412+
try:
413+
if isinstance(key, dict) and key.get('type') == 'string_literal':
414+
key = key['value']
415+
return container[key]
416+
except KeyError:
417+
raise ThinkRuntimeError(
418+
message=f"Invalid index/key: {key} not found in dictionary",
419+
task=self.current_task,
420+
step=self.current_step,
421+
variables={
422+
"attempted_key": key,
423+
"available_keys": list(container.keys())
424+
}
425+
)
426+
427+
# Handle invalid container types
376428
else:
377429
raise ThinkRuntimeError(
378-
message=f"Cannot index into type: {type(container)}",
430+
message=f"Cannot index into type: {type(container).__name__}",
379431
task=self.current_task,
380432
step=self.current_step,
381433
variables={
382434
"attempted_type": type(container).__name__,
383-
"indexable_types": ["list", "dict", "string"],
384-
"value_attempted": str(container)
435+
"value": str(container),
436+
"indexable_types": ["list", "dict"]
385437
}
386438
)
387439

@@ -407,6 +459,8 @@ def evaluate_expression(self, expr):
407459

408460
return expr
409461

462+
463+
410464
def evaluate_operation(self, operation):
411465
"""Evaluate a mathematical or logical operation"""
412466
op = operation['operator']

0 commit comments

Comments
 (0)