From 6e5ce1843563ea52c8a22913b44f8740c15a7905 Mon Sep 17 00:00:00 2001 From: Aidan <46799759+PossiblyAShrub@users.noreply.github.com> Date: Thu, 24 Oct 2024 19:11:29 -0600 Subject: [PATCH] [ysh breaking] 1 .. 5 range replaced with 1..<5 half open and 1..=5 closed range (#2102) This is issue #2096 --- display/pp_value.py | 2 +- doc/error-catalog.md | 26 +++++++++++++++++++++++ doc/ref/chap-expr-lang.md | 14 +++++++----- doc/ref/toc-ysh.md | 2 +- doc/ysh-tour.md | 4 ++-- frontend/id_kind_def.py | 5 +++-- frontend/lexer_def.py | 10 +++++---- spec/ysh-bugs.test.sh | 8 +++---- spec/ysh-builtins.test.sh | 2 +- spec/ysh-closures.test.sh | 6 +++--- spec/ysh-convert.test.sh | 4 ++-- spec/ysh-for.test.sh | 2 +- spec/ysh-func.test.sh | 2 +- spec/ysh-int-float.test.sh | 2 +- spec/ysh-json.test.sh | 4 ++-- spec/ysh-list.test.sh | 10 ++++----- spec/ysh-methods.test.sh | 2 +- spec/ysh-printing.test.sh | 8 +++---- spec/ysh-regex-api.test.sh | 2 +- spec/ysh-slice-range.test.sh | 41 ++++++++++++++++++++++++------------ spec/ysh-word-eval.test.sh | 2 +- stdlib/ysh/list-test.ysh | 6 +++--- stdlib/ysh/list.ysh | 4 ++-- stdlib/ysh/math.ysh | 2 +- test/ysh-parse-errors.sh | 6 ++++++ test/ysh-runtime-errors.sh | 6 +++--- ysh/expr_eval.py | 3 +++ ysh/expr_parse.py | 4 ++++ ysh/grammar.pgen2 | 5 ++++- ysh/grammar_gen.py | 3 ++- 30 files changed, 130 insertions(+), 67 deletions(-) diff --git a/display/pp_value.py b/display/pp_value.py index f4bf01eef..aead5eaf2 100644 --- a/display/pp_value.py +++ b/display/pp_value.py @@ -412,7 +412,7 @@ def _Value(self, val): elif case(value_e.Range): r = cast(value.Range, val) type_name = self._Styled(self.type_style, UText(ValType(r))) - mdocs = [UText(str(r.lower)), UText(".."), UText(str(r.upper))] + mdocs = [UText(str(r.lower)), UText("..<"), UText(str(r.upper))] return self._SurroundedAndPrefixed("(", type_name, " ", self._Join(mdocs, "", " "), ")") diff --git a/doc/error-catalog.md b/doc/error-catalog.md index cbadb9107..dabf2d4bb 100644 --- a/doc/error-catalog.md +++ b/doc/error-catalog.md @@ -207,6 +207,32 @@ standard boolean operators are written as `a and b`, `a or b` and `not a`. This differs from [command mode](command-vs-expression-mode.html) which uses shell-like `||` for "OR", `&&` for "AND" and `!` for "NOT". +### OILS-ERR-16 + +``` + for x in (1 .. 5) { + ^~ +[ -c flag ]:1: Use ..< for half-open range, or ..= for closed range (OILS-ERR-16) +``` + +There are two ways to construct a [Range](ref/chap-expr-lang#range). The `..<` +operator is for half-open ranges and the `..=` operator is for closed ranges: + + for i in (0 ..< 3) { + echo $i + } + => 0 + => 1 + => 2 + + for i in (0 ..= 3) { + echo $i + } + => 0 + => 1 + => 2 + => 3 + ## Runtime Errors - Traditional Shell These errors may occur in shells like [bash]($xref) and [zsh]($xref). diff --git a/doc/ref/chap-expr-lang.md b/doc/ref/chap-expr-lang.md index 5b29c2833..ed90a5cfb 100644 --- a/doc/ref/chap-expr-lang.md +++ b/doc/ref/chap-expr-lang.md @@ -231,21 +231,25 @@ the same name: ### range -A range is a sequence of numbers that can be iterated over: +A Range is a sequence of numbers that can be iterated over. The `..<` operator +constructs half-open ranges. - for i in (0 .. 3) { + for i in (0 ..< 3) { echo $i } => 0 => 1 => 2 -As with slices, the last number isn't included. To iterate from 1 to n, you -can use this idiom: +The `..=` operator constructs closed ranges: - for i in (1 .. n+1) { + for i in (0 ..= 3) { echo $i } + => 0 + => 1 + => 2 + => 3 ### block-expr diff --git a/doc/ref/toc-ysh.md b/doc/ref/toc-ysh.md index 1afca9767..424dccf59 100644 --- a/doc/ref/toc-ysh.md +++ b/doc/ref/toc-ysh.md @@ -262,7 +262,7 @@ X [External Lang] BEGIN END when (awk) str-template ^"$a and $b" for Str::replace() list-literal ['one', 'two', 3] :| unquoted words | dict-literal {name: 'bob'} {a, b} - range 1 .. n+1 + range 1 ..< n 1 ..= n block-expr ^(echo $PWD) expr-literal ^[1 + 2*3] X expr-sub $[myobj] diff --git a/doc/ysh-tour.md b/doc/ysh-tour.md index 326236067..0347f729e 100644 --- a/doc/ysh-tour.md +++ b/doc/ysh-tour.md @@ -453,7 +453,7 @@ Ask for the loop index: To iterate over a typed data, use parentheses around an **expression**. The expression should evaluate to an integer `Range`, `List`, or `Dict`: - for i in (3 .. 5) { # range operator .. + for i in (3 ..< 5) { # range operator ..< echo "i = $i" } # => @@ -740,7 +740,7 @@ Here's a pure function: func myRepeat(s, n; special=false) { # positional; named params var parts = [] - for i in (0 .. n) { + for i in (0 ..< n) { append $s (parts) } var result = join(parts) diff --git a/frontend/id_kind_def.py b/frontend/id_kind_def.py index 1d87fb6fd..22e5e3f1f 100755 --- a/frontend/id_kind_def.py +++ b/frontend/id_kind_def.py @@ -232,7 +232,7 @@ def AddKinds(spec): # $'\z' Such bad codes are accepted when parse_backslash is on # (default in OSH), so we have to lex them. # (x == y) should used === or ~== - spec.AddKind('Unknown', ['Tok', 'Backslash', 'DEqual', 'DAmp', 'DPipe']) + spec.AddKind('Unknown', ['Tok', 'Backslash', 'DEqual', 'DAmp', 'DPipe', 'DDot']) spec.AddKind('Eol', ['Tok']) # no more tokens on line (\0) @@ -333,7 +333,8 @@ def AddKinds(spec): 'Float', 'Bang', # eggex !digit, ![a-z] 'Dot', - 'DDot', + 'DDotLessThan', + 'DDotEqual', 'Colon', # mylist:pop() 'RArrow', 'RDArrow', diff --git a/frontend/lexer_def.py b/frontend/lexer_def.py index 0cc0b6123..fc4e875aa 100644 --- a/frontend/lexer_def.py +++ b/frontend/lexer_def.py @@ -1103,10 +1103,12 @@ def R(pat, tok_type): C('//', Id.Expr_DSlash), # For YSH integer division C('~==', Id.Expr_TildeDEqual), # approximate equality - C('.', Id.Expr_Dot), # d.key is alias for d['key'] - C('..', Id.Expr_DDot), # range 1..5 - C('->', Id.Expr_RArrow), # s->startswith() - C('$', Id.Expr_Dollar), # legacy regex end: /d+ $/ (better written /d+ >/ + C('.', Id.Expr_Dot), # d.key is alias for d['key'] + C('..', Id.Unknown_DDot), # legacy half-open range 1..5 + C('..<', Id.Expr_DDotLessThan), # half-open range 1..<5 + C('..=', Id.Expr_DDotEqual), # closed range 1..5 + C('->', Id.Expr_RArrow), # s->startswith() + C('$', Id.Expr_Dollar), # legacy regex end: /d+ $/ (better written /d+ >/ # Reserved this. Go uses it for channels, etc. # I guess it conflicts with -4<-3, but that's OK -- spaces suffices. diff --git a/spec/ysh-bugs.test.sh b/spec/ysh-bugs.test.sh index 2227d038f..ce8712d5b 100644 --- a/spec/ysh-bugs.test.sh +++ b/spec/ysh-bugs.test.sh @@ -142,7 +142,7 @@ pp test_ (pipe()) #### shvar then replace - bug #1986 context manager crash shvar FOO=bar { - for x in (1 .. 500) { + for x in (1 ..< 500) { var Q = "hello" setvar Q = Q=>replace("hello","world") } @@ -239,20 +239,20 @@ case (WEIGHT) { proc p { var s = "hi" - for q in (1..50) { + for q in (1..<50) { shvar Q="whatever" { setvar s = "." ++ s } } } -for i in (1..10) { +for i in (1..<10) { p } if false { echo 'testing for longer' - for i in (1 .. 1000) { + for i in (1 ..< 1000) { p } } diff --git a/spec/ysh-builtins.test.sh b/spec/ysh-builtins.test.sh index 1fbfc32b6..cb03b5ee5 100644 --- a/spec/ysh-builtins.test.sh +++ b/spec/ysh-builtins.test.sh @@ -638,7 +638,7 @@ echo $[type(f)] echo $[type(len)] echo $[type('foo'=>startsWith)] echo $[type('foo'=>join)] # Type error happens later -echo $[type(1..3)] +echo $[type(1..<3)] ## STDOUT: Int Str diff --git a/spec/ysh-closures.test.sh b/spec/ysh-closures.test.sh index 4e37bd4cc..ec88cd211 100644 --- a/spec/ysh-closures.test.sh +++ b/spec/ysh-closures.test.sh @@ -50,7 +50,7 @@ proc task (; tasks, expr) { func makeTasks() { var tasks = [] var x = 'x' - for __hack__ in (0 .. 3) { + for __hack__ in (0 ..< 3) { var i = __hack__ var j = i + 2 task (tasks, ^"$x: i = $i, j = $j") @@ -82,7 +82,7 @@ proc task (; tasks; ; b) { func makeTasks() { var tasks = [] var x = 'x' - for __hack__ in (0 .. 3) { + for __hack__ in (0 ..< 3) { var i = __hack__ var j = i + 2 task (tasks) { echo "$x: i = $i, j = $j" } @@ -108,7 +108,7 @@ x: i = 2, j = 4 shopt --set ysh:upgrade var procs = [] -for i in (0 .. 3) { +for i in (0 ..< 3) { proc __invoke__ (; self) { echo "i = $[self.i]" } diff --git a/spec/ysh-convert.test.sh b/spec/ysh-convert.test.sh index 9e102c9f8..145b4bf60 100644 --- a/spec/ysh-convert.test.sh +++ b/spec/ysh-convert.test.sh @@ -9,7 +9,7 @@ echo "$[bool({})]" echo "$[bool(null)]" echo "$[bool(len)]" echo "$[bool('foo'=>startsWith)]" -echo "$[bool(1..3)]" +echo "$[bool(1..<3)]" ## STDOUT: true false @@ -180,7 +180,7 @@ foo #### list() from range shopt -s ysh:upgrade -var mylist = list(0..3) +var mylist = list(0..<3) write @mylist ## STDOUT: 0 diff --git a/spec/ysh-for.test.sh b/spec/ysh-for.test.sh index dd6d77d71..0a57f58ea 100644 --- a/spec/ysh-for.test.sh +++ b/spec/ysh-for.test.sh @@ -33,7 +33,7 @@ key age #### For loop over range -var myrange = 0 .. 3 +var myrange = 0 ..< 3 for i in (myrange) { echo "i $i" } diff --git a/spec/ysh-func.test.sh b/spec/ysh-func.test.sh index 53c9c9c9f..ffafc6458 100644 --- a/spec/ysh-func.test.sh +++ b/spec/ysh-func.test.sh @@ -250,7 +250,7 @@ var cache = [] var maxSize = 4 func remove(l, i) { - for i in (i .. len(l) - 1) { + for i in (i ..< len(l) - 1) { setvar l[i] = l[i + 1] } diff --git a/spec/ysh-int-float.test.sh b/spec/ysh-int-float.test.sh index 6e06e2ad9..92453e586 100644 --- a/spec/ysh-int-float.test.sh +++ b/spec/ysh-int-float.test.sh @@ -80,7 +80,7 @@ shopt -s ysh:upgrade # 1e-324 == 0.0 in Python var zeros = [] -for i in (1 .. 324) { +for i in (1 ..< 324) { call zeros->append('0') } diff --git a/spec/ysh-json.test.sh b/spec/ysh-json.test.sh index 08b60c13f..26c7daa41 100644 --- a/spec/ysh-json.test.sh +++ b/spec/ysh-json.test.sh @@ -1168,10 +1168,10 @@ shopt -s ysh:upgrade proc pairs(n) { var m = int(n) # TODO: 1 .. n should auto-convert? - for i in (1 .. m) { + for i in (1 ..< m) { write -n -- '[' } - for i in (1 .. m) { + for i in (1 ..< m) { write -n -- ']' } } diff --git a/spec/ysh-list.test.sh b/spec/ysh-list.test.sh index bf985a01e..299b61dee 100644 --- a/spec/ysh-list.test.sh +++ b/spec/ysh-list.test.sh @@ -140,9 +140,9 @@ echo -$x- # fails with type error ## END #### List->extend() -var l = list(1..3) +var l = list(1..<3) echo $[len(l)] -call l->extend(list(3..6)) +call l->extend(list(3..<6)) echo $[len(l)] ## STDOUT: 2 @@ -151,9 +151,9 @@ echo $[len(l)] #### List append()/extend() should return null shopt -s ysh:all -var l = list(1..3) +var l = list(1..<3) -var result = l->extend(list(3..6)) +var result = l->extend(list(3..<6)) assert [null === result] setvar result = l->append(6) @@ -166,7 +166,7 @@ pass #### List pop() shopt -s ysh:all -var l = list(1..5) +var l = list(1..<5) assert [4 === l->pop()] assert [3 === l->pop()] assert [2 === l->pop()] diff --git a/spec/ysh-methods.test.sh b/spec/ysh-methods.test.sh index 291f67121..5db754d15 100644 --- a/spec/ysh-methods.test.sh +++ b/spec/ysh-methods.test.sh @@ -584,7 +584,7 @@ pp test_ (c) ## END #### List->reverse() from iterator -var x = list(0 .. 3) +var x = list(0 ..< 3) call x->reverse() write @x ## STDOUT: diff --git a/spec/ysh-printing.test.sh b/spec/ysh-printing.test.sh index cc866baa4..322b818c2 100644 --- a/spec/ysh-printing.test.sh +++ b/spec/ysh-printing.test.sh @@ -35,11 +35,11 @@ ## END #### Range -var x = 1..100 +var x = 1..<100 pp value (x) -# TODO: show type here, like (Range 1 .. 100) +# TODO: show type here, like (Range 1 ..< 100) pp value ({k: x}) @@ -49,8 +49,8 @@ pp test_ (x) pp test_ ({k: x}) ## STDOUT: -(Range 1 .. 100) -(Dict) {k: (Range 1 .. 100)} +(Range 1 ..< 100) +(Dict) {k: (Range 1 ..< 100)} (Dict) {"k":} diff --git a/spec/ysh-regex-api.test.sh b/spec/ysh-regex-api.test.sh index 25a0c81d7..c149eb056 100644 --- a/spec/ysh-regex-api.test.sh +++ b/spec/ysh-regex-api.test.sh @@ -752,7 +752,7 @@ shopt --set ysh:all var mystr = '1abc2abc3abc' -for count in (-2..4) { +for count in (-2..<4) { write $[mystr.replace('abc', "-", count=count)] write $[mystr.replace('abc', ^"-", count=count)] write $[mystr.replace(/ [a-z]+ /, "-", count=count)] diff --git a/spec/ysh-slice-range.test.sh b/spec/ysh-slice-range.test.sh index fb810a967..d2ed0c836 100644 --- a/spec/ysh-slice-range.test.sh +++ b/spec/ysh-slice-range.test.sh @@ -14,16 +14,16 @@ # >>> xrange(1,3) < xrange(1,4) # True -= 1..3 += 1..<3 ## STDOUT: -(Range 1 .. 3) +(Range 1 ..< 3) ## END #### precedence of 1:3 vs bitwise operator -= 3..3|4 += 3..<3|4 ## STDOUT: -(Range 3 .. 7) +(Range 3 ..< 7) ## END #### subscript and slice :| 1 2 3 4 | @@ -57,20 +57,20 @@ out of bounds #### Range end points can be int-looking Strings -pp test_ (list('3' .. '6')) +pp test_ (list('3' ..< '6')) var i = '5' -pp test_ (list(i .. 7)) -pp test_ (list(3 .. i)) +pp test_ (list(i ..< 7)) +pp test_ (list(3 ..< i)) var i = '-5' -pp test_ (list(i .. -3)) -pp test_ (list(-7 .. i)) +pp test_ (list(i ..< -3)) +pp test_ (list(-7 ..< i)) # Not allowed -pp test_ ('a' .. 'z') +pp test_ ('a' ..< 'z') ## status: 3 ## STDOUT: @@ -83,7 +83,7 @@ pp test_ ('a' .. 'z') #### Slice indices can be int-looking strings -var a = list(0..10) +var a = list(0..<10) #pp test_ (a) pp test_ (a['3': '6']) @@ -215,10 +215,10 @@ pp test_ (b) ## END #### Iterate over range -for i in (1..5) { +for i in (1..<5) { echo $[i] } -for i, n in (1..4) { +for i, n in (1..<4) { echo "$[i], $[n]" } ## STDOUT: @@ -234,7 +234,7 @@ for i, n in (1..4) { #### Loops over bogus ranges terminate # Regression test for bug found during dev. Loops over backwards ranges should # terminate immediately. -for i in (5..1) { +for i in (5..<1) { echo $[i] } ## STDOUT: @@ -263,3 +263,16 @@ var t3 = mytable[:2, %(name age)] (Str) 'TODO: Table Slicing' (Str) 'TODO: Table Slicing' ## END + +#### Closed ranges + +for x in (1..=2) { + echo $x +} + += 1..=2 +## STDOUT: +1 +2 +(Range 1 ..< 3) +## END diff --git a/spec/ysh-word-eval.test.sh b/spec/ysh-word-eval.test.sh index f124c20ac..cf8dd4358 100644 --- a/spec/ysh-word-eval.test.sh +++ b/spec/ysh-word-eval.test.sh @@ -104,7 +104,7 @@ true #### Wrong sigil with $range() is runtime error shopt -s ysh:upgrade -echo $[10 .. 15] +echo $[10 ..< 15] echo 'should not get here' ## status: 3 ## STDOUT: diff --git a/stdlib/ysh/list-test.ysh b/stdlib/ysh/list-test.ysh index 4fc08bec8..8aff90bb6 100755 --- a/stdlib/ysh/list-test.ysh +++ b/stdlib/ysh/list-test.ysh @@ -45,9 +45,9 @@ proc test-sum { assert [0 === sum([0])] assert [6 === sum([1, 2, 3])] - assert [3 === sum( 0 .. 3 )] - assert [45 === sum( 0 .. 3; start=42)] - assert [42 === sum( 0 .. 0, start=42)] + assert [3 === sum( 0 ..< 3 )] + assert [45 === sum( 0 ..< 3; start=42)] + assert [42 === sum( 0 ..< 0, start=42)] } proc test-repeat-str { diff --git a/stdlib/ysh/list.ysh b/stdlib/ysh/list.ysh index bfb84586b..5ce8ba251 100644 --- a/stdlib/ysh/list.ysh +++ b/stdlib/ysh/list.ysh @@ -45,14 +45,14 @@ func repeat(x, n) { case (t) { Str { var parts = [] - for i in (0 .. n) { + for i in (0 ..< n) { call parts->append(x) } return (join(parts)) } List { var result = [] - for i in (0 .. n) { + for i in (0 ..< n) { call result->extend(x) } return (result) diff --git a/stdlib/ysh/math.ysh b/stdlib/ysh/math.ysh index 4cd28fb71..189a62a8e 100644 --- a/stdlib/ysh/math.ysh +++ b/stdlib/ysh/math.ysh @@ -20,7 +20,7 @@ func __math_select(list, cmp) { } var match = list[0] - for i in (1 .. len(list)) { + for i in (1 ..< len(list)) { setvar match = cmp(list[i], match) } return (match) diff --git a/test/ysh-parse-errors.sh b/test/ysh-parse-errors.sh index 88c8f4216..cf0f1ffbc 100755 --- a/test/ysh-parse-errors.sh +++ b/test/ysh-parse-errors.sh @@ -1690,6 +1690,12 @@ test-unknown-boolops() { _osh-parse-error '= !a' } +test-expr-range() { + _osh-parse-error '= 1..5' + _osh-should-parse '= 1..<5' + _osh-should-parse '= 1..=5' +} + # # Entry Points # diff --git a/test/ysh-runtime-errors.sh b/test/ysh-runtime-errors.sh index 5cca8c4a3..0d04c33a3 100755 --- a/test/ysh-runtime-errors.sh +++ b/test/ysh-runtime-errors.sh @@ -991,10 +991,10 @@ test-assert() { _ysh-expr-error 'assert [null === 42]' # One is long - _ysh-expr-error 'assert [null === list(1 .. 50)]' + _ysh-expr-error 'assert [null === list(1 ..< 50)]' # Both are long - _ysh-expr-error 'assert [{k: list(3 .. 40)} === list(1 .. 50)]' + _ysh-expr-error 'assert [{k: list(3 ..< 40)} === list(1 ..< 50)]' } test-pp() { @@ -1013,7 +1013,7 @@ var x = 42; pp [x]' _ysh-should-run ' -var x = list(1 .. 50); +var x = list(1 ..< 50); pp [x]' } diff --git a/ysh/expr_eval.py b/ysh/expr_eval.py index 181060cff..4f8489b77 100644 --- a/ysh/expr_eval.py +++ b/ysh/expr_eval.py @@ -1280,6 +1280,9 @@ def _EvalExpr(self, node): i2 = _ConvertToInt(self._EvalExpr(node.upper), 'Range end should be Int', node.op) + if node.op.id == Id.Expr_DDotEqual: # Closed range + i2 = mops.Add(i2, mops.ONE) + # TODO: Don't truncate return value.Range(mops.BigTruncate(i1), mops.BigTruncate(i2)) diff --git a/ysh/expr_parse.py b/ysh/expr_parse.py index 2afb3308d..496d56690 100644 --- a/ysh/expr_parse.py +++ b/ysh/expr_parse.py @@ -79,12 +79,16 @@ def _Classify(gr, tok): if id_ == Id.Unknown_DEqual: p_die('Use === to be exact, or ~== to convert types', tok) + if id_ == Id.Unknown_DAmp: p_die("Use 'and' in expression mode (OILS-ERR-15)", tok) if id_ == Id.Unknown_DPipe: p_die("Use 'or' in expression mode (OILS-ERR-15)", tok) # Not possible to check '!' as it conflicts with Id.Expr_Bang + if id_ == Id.Unknown_DDot: + p_die('Use ..< for half-open range, or ..= for closed range (OILS-ERR-16)', tok) + if id_ == Id.Unknown_Tok: type_str = '' else: diff --git a/ysh/grammar.pgen2 b/ysh/grammar.pgen2 index a07d8957d..d38cf157e 100644 --- a/ysh/grammar.pgen2 +++ b/ysh/grammar.pgen2 @@ -54,7 +54,10 @@ not_test: 'not' not_test | comparison comparison: range_expr (comp_op range_expr)* # Unlike slice, beginning and end are required -range_expr: expr ['..' expr] +range_expr: ( + expr ['..<' expr] | + expr ['..=' expr] +) # YSH patch: remove legacy <>, add === and more comp_op: ( diff --git a/ysh/grammar_gen.py b/ysh/grammar_gen.py index 781cdd272..a8a817de5 100755 --- a/ysh/grammar_gen.py +++ b/ysh/grammar_gen.py @@ -75,7 +75,8 @@ def main(argv): OPS = { '!': Id.Expr_Bang, '.': Id.Expr_Dot, - '..': Id.Expr_DDot, + '..=': Id.Expr_DDotEqual, + '..<': Id.Expr_DDotLessThan, '->': Id.Expr_RArrow, '=>': Id.Expr_RDArrow, '//': Id.Expr_DSlash,