Skip to content

Commit 331b0b4

Browse files
authored
Merge pull request #1099 from SchoolyB/bugfixes/range-modules-and-warnings
fix: `range()`, module imports, and error file tracking (#1093, #1094, #1095) tests: fix failing time module integration tests
2 parents 07f185c + 6cd26f2 commit 331b0b4

4 files changed

Lines changed: 316 additions & 18 deletions

File tree

pkg/interpreter/evaluator.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1833,7 +1833,7 @@ func evalRangeExpression(node *ast.RangeExpression, env *Environment) Object {
18331833
}
18341834
end := new(big.Int).Set(endInt.Value)
18351835

1836-
// Handle step - defaults to 1 (or -1 for descending ranges)
1836+
// Handle step - defaults to 1
18371837
step := big.NewInt(1)
18381838
if node.Step != nil {
18391839
stepObj := Eval(node.Step, env)
@@ -1850,9 +1850,18 @@ func evalRangeExpression(node *ast.RangeExpression, env *Environment) Object {
18501850
return newErrorWithLocation("E9003", line, col,
18511851
"range step cannot be zero")
18521852
}
1853-
} else if start.Cmp(end) > 0 {
1854-
// Auto-detect descending range when no step is provided
1855-
step = big.NewInt(-1)
1853+
}
1854+
1855+
// Validate step direction matches bounds (#1095)
1856+
if step.Sign() > 0 && start.Cmp(end) > 0 {
1857+
return newErrorWithLocation("E9005", line, col,
1858+
"invalid range: start (%s) must be <= end (%s) for positive step",
1859+
start.String(), end.String())
1860+
}
1861+
if step.Sign() < 0 && start.Cmp(end) < 0 {
1862+
return newErrorWithLocation("E9005", line, col,
1863+
"invalid range: start (%s) must be >= end (%s) for negative step",
1864+
start.String(), end.String())
18561865
}
18571866

18581867
return &Range{Start: start, End: end, Step: step}
@@ -1919,6 +1928,10 @@ func evalSingleCast(value Object, targetType string, line, col int) Object {
19191928
errObj.Line = line
19201929
errObj.Column = col
19211930
}
1931+
// Set file from current context if not already set (#1094)
1932+
if errObj.File == "" && globalEvalContext != nil {
1933+
errObj.File = globalEvalContext.CurrentFile
1934+
}
19221935
}
19231936
return result
19241937
}
@@ -1966,7 +1979,7 @@ func evalForStatement(node *ast.ForStatement, env *Environment) Object {
19661979
}
19671980
end := endInt.Value
19681981

1969-
// Handle step - defaults to 1 (or -1 for descending ranges)
1982+
// Handle step - defaults to 1
19701983
step := big.NewInt(1)
19711984
if rangeExpr.Step != nil {
19721985
stepObj := Eval(rangeExpr.Step, env)
@@ -1983,9 +1996,18 @@ func evalForStatement(node *ast.ForStatement, env *Environment) Object {
19831996
return newErrorWithLocation("E9003", node.Token.Line, node.Token.Column,
19841997
"range step cannot be zero")
19851998
}
1986-
} else if start.Cmp(end) > 0 {
1987-
// Auto-detect descending range when no step is provided
1988-
step = big.NewInt(-1)
1999+
}
2000+
2001+
// Validate step direction matches bounds (#1095)
2002+
if step.Sign() > 0 && start.Cmp(end) > 0 {
2003+
return newErrorWithLocation("E9005", node.Token.Line, node.Token.Column,
2004+
"invalid range: start (%s) must be <= end (%s) for positive step",
2005+
start.String(), end.String())
2006+
}
2007+
if step.Sign() < 0 && start.Cmp(end) < 0 {
2008+
return newErrorWithLocation("E9005", node.Token.Line, node.Token.Column,
2009+
"invalid range: start (%s) must be >= end (%s) for negative step",
2010+
start.String(), end.String())
19892011
}
19902012

19912013
loopEnv := NewEnclosedEnvironment(env)
@@ -2961,6 +2983,10 @@ func evalMemberCall(member *ast.MemberExpression, args []ast.Expression, env *En
29612983
errObj.Line = member.Token.Line
29622984
errObj.Column = member.Token.Column
29632985
}
2986+
// Set file from current context if not already set (#1094)
2987+
if errObj.File == "" && globalEvalContext != nil {
2988+
errObj.File = globalEvalContext.CurrentFile
2989+
}
29642990
}
29652991
return result
29662992
}
@@ -3078,6 +3104,10 @@ func applyFunction(fn Object, args []Object, line, col int) Object {
30783104
errObj.Line = line
30793105
errObj.Column = col
30803106
}
3107+
// Set file from current context if not already set (#1094)
3108+
if errObj.File == "" && globalEvalContext != nil {
3109+
errObj.File = globalEvalContext.CurrentFile
3110+
}
30813111
}
30823112
return result
30833113

pkg/interpreter/evaluator_test.go

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5750,3 +5750,153 @@ temp _ int = track()
57505750
// but since we're returning an error, we can't check the count.
57515751
// This test verifies that ensure doesn't prevent error propagation.
57525752
}
5753+
5754+
// ============================================================================
5755+
// Range with Negative Step Tests (#1095)
5756+
// ============================================================================
5757+
5758+
func TestRangeWithNegativeStep(t *testing.T) {
5759+
input := `
5760+
temp sum int = 0
5761+
for i in range(10, 0, -1) {
5762+
sum = sum + i
5763+
}
5764+
sum
5765+
`
5766+
evaluated := testEval(input)
5767+
// 10 + 9 + 8 + 7 + 6 + 5 + 4 + 3 + 2 + 1 = 55
5768+
testIntegerObject(t, evaluated, 55)
5769+
}
5770+
5771+
func TestRangeWithNegativeStepBy2(t *testing.T) {
5772+
input := `
5773+
temp sum int = 0
5774+
for i in range(10, 0, -2) {
5775+
sum = sum + i
5776+
}
5777+
sum
5778+
`
5779+
evaluated := testEval(input)
5780+
// 10 + 8 + 6 + 4 + 2 = 30
5781+
testIntegerObject(t, evaluated, 30)
5782+
}
5783+
5784+
func TestRangeNegativeStepCollectsValues(t *testing.T) {
5785+
input := `
5786+
temp first int = 0
5787+
temp second int = 0
5788+
temp third int = 0
5789+
temp idx int = 0
5790+
for i in range(5, 2, -1) {
5791+
if idx == 0 { first = i }
5792+
if idx == 1 { second = i }
5793+
if idx == 2 { third = i }
5794+
idx = idx + 1
5795+
}
5796+
first * 100 + second * 10 + third
5797+
`
5798+
evaluated := testEval(input)
5799+
// first=5, second=4, third=3 -> 543
5800+
testIntegerObject(t, evaluated, 543)
5801+
}
5802+
5803+
func TestRangeNegativeStepMismatchError(t *testing.T) {
5804+
// Negative step with ascending bounds should error
5805+
input := `
5806+
for i in range(0, 10, -1) {
5807+
temp _ int = i
5808+
}
5809+
`
5810+
evaluated := testEval(input)
5811+
if !isError(evaluated) {
5812+
t.Fatalf("expected error for mismatched step direction, got %T", evaluated)
5813+
}
5814+
errObj := evaluated.(*Error)
5815+
if errObj.Code != "E9005" {
5816+
t.Errorf("expected error code E9005, got %s", errObj.Code)
5817+
}
5818+
}
5819+
5820+
func TestRangePositiveStepMismatchError(t *testing.T) {
5821+
// Positive step (default) with descending bounds should error
5822+
input := `
5823+
for i in range(10, 0) {
5824+
temp _ int = i
5825+
}
5826+
`
5827+
evaluated := testEval(input)
5828+
if !isError(evaluated) {
5829+
t.Fatalf("expected error for mismatched step direction, got %T", evaluated)
5830+
}
5831+
errObj := evaluated.(*Error)
5832+
if errObj.Code != "E9005" {
5833+
t.Errorf("expected error code E9005, got %s", errObj.Code)
5834+
}
5835+
}
5836+
5837+
func TestRangeZeroStepError(t *testing.T) {
5838+
input := `
5839+
for i in range(0, 10, 0) {
5840+
temp _ int = i
5841+
}
5842+
`
5843+
evaluated := testEval(input)
5844+
if !isError(evaluated) {
5845+
t.Fatalf("expected error for zero step, got %T", evaluated)
5846+
}
5847+
errObj := evaluated.(*Error)
5848+
if errObj.Code != "E9003" {
5849+
t.Errorf("expected error code E9003, got %s", errObj.Code)
5850+
}
5851+
}
5852+
5853+
func TestRangeNegativeStepInOperator(t *testing.T) {
5854+
tests := []struct {
5855+
name string
5856+
input string
5857+
expected bool
5858+
}{
5859+
{
5860+
name: "value in descending range - true",
5861+
input: `5 in range(10, 0, -1)`,
5862+
expected: true,
5863+
},
5864+
{
5865+
name: "value at start of descending range - true",
5866+
input: `10 in range(10, 0, -1)`,
5867+
expected: true,
5868+
},
5869+
{
5870+
name: "value at end of descending range - false (exclusive)",
5871+
input: `0 in range(10, 0, -1)`,
5872+
expected: false,
5873+
},
5874+
{
5875+
name: "value below descending range - false",
5876+
input: `-1 in range(10, 0, -1)`,
5877+
expected: false,
5878+
},
5879+
{
5880+
name: "value above descending range - false",
5881+
input: `15 in range(10, 0, -1)`,
5882+
expected: false,
5883+
},
5884+
{
5885+
name: "value in descending range with step -2 - on step",
5886+
input: `6 in range(10, 0, -2)`,
5887+
expected: true,
5888+
},
5889+
{
5890+
name: "value in descending range with step -2 - off step",
5891+
input: `5 in range(10, 0, -2)`,
5892+
expected: false,
5893+
},
5894+
}
5895+
5896+
for _, tt := range tests {
5897+
t.Run(tt.name, func(t *testing.T) {
5898+
evaluated := testEval(tt.input)
5899+
testBooleanObject(t, evaluated, tt.expected)
5900+
})
5901+
}
5902+
}

pkg/stdlib/stdlib_test.go

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1985,7 +1985,9 @@ func TestTimeToUnixMs(t *testing.T) {
19851985
func TestTimeIsWeekend(t *testing.T) {
19861986
isWeekendFn := TimeBuiltins["time.is_weekend"].Fn
19871987

1988-
saturday := big.NewInt(1578096000)
1988+
// Use midday UTC timestamps to avoid timezone boundary issues
1989+
// Saturday, January 4, 2020 12:00:00 UTC
1990+
saturday := big.NewInt(1578139200)
19891991
result := isWeekendFn(&object.Integer{Value: saturday})
19901992

19911993
boolVal, ok := result.(*object.Boolean)
@@ -1996,7 +1998,8 @@ func TestTimeIsWeekend(t *testing.T) {
19961998
t.Errorf("time.is_weekend() returned false, want true")
19971999
}
19982000

1999-
sunday := big.NewInt(1578182400)
2001+
// Sunday, January 5, 2020 12:00:00 UTC
2002+
sunday := big.NewInt(1578225600)
20002003
result = isWeekendFn(&object.Integer{Value: sunday})
20012004
boolVal, ok = result.(*object.Boolean)
20022005
if !ok {
@@ -2006,7 +2009,8 @@ func TestTimeIsWeekend(t *testing.T) {
20062009
t.Errorf("time.is_weekend() returned false, want true")
20072010
}
20082011

2009-
monday := big.NewInt(1578268800)
2012+
// Monday, January 6, 2020 12:00:00 UTC
2013+
monday := big.NewInt(1578312000)
20102014
result = isWeekendFn(&object.Integer{Value: monday})
20112015
boolVal, ok = result.(*object.Boolean)
20122016
if !ok {
@@ -2020,7 +2024,9 @@ func TestTimeIsWeekend(t *testing.T) {
20202024
func TestTimeIsWeekday(t *testing.T) {
20212025
isWeekdayFn := TimeBuiltins["time.is_weekday"].Fn
20222026

2023-
monday := big.NewInt(1578268800)
2027+
// Use midday UTC timestamps to avoid timezone boundary issues
2028+
// Monday, January 6, 2020 12:00:00 UTC
2029+
monday := big.NewInt(1578312000)
20242030
result := isWeekdayFn(&object.Integer{Value: monday})
20252031
boolVal, ok := result.(*object.Boolean)
20262032
if !ok {
@@ -2030,7 +2036,8 @@ func TestTimeIsWeekday(t *testing.T) {
20302036
t.Errorf("time.is_weekday() returned false, want true")
20312037
}
20322038

2033-
tuesday := big.NewInt(1578355200)
2039+
// Tuesday, January 7, 2020 12:00:00 UTC
2040+
tuesday := big.NewInt(1578398400)
20342041
result = isWeekdayFn(&object.Integer{Value: tuesday})
20352042
boolVal, ok = result.(*object.Boolean)
20362043
if !ok {
@@ -2040,7 +2047,8 @@ func TestTimeIsWeekday(t *testing.T) {
20402047
t.Errorf("time.is_weekday() returned false, want true")
20412048
}
20422049

2043-
saturday := big.NewInt(1578096000)
2050+
// Saturday, January 4, 2020 12:00:00 UTC
2051+
saturday := big.NewInt(1578139200)
20442052
result = isWeekdayFn(&object.Integer{Value: saturday})
20452053
boolVal, ok = result.(*object.Boolean)
20462054
if !ok {

0 commit comments

Comments
 (0)