rush-shell 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (210) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +94 -0
  4. data/exe/rush +10 -0
  5. data/grammar/shell.y +272 -0
  6. data/grammar/shell.y.output +2354 -0
  7. data/lib/rush/alias_table.rb +48 -0
  8. data/lib/rush/assignments.rb +48 -0
  9. data/lib/rush/ast/and_or.rb +37 -0
  10. data/lib/rush/ast/assignment.rb +20 -0
  11. data/lib/rush/ast/brace_group.rb +25 -0
  12. data/lib/rush/ast/case_node.rb +41 -0
  13. data/lib/rush/ast/condition_loop.rb +34 -0
  14. data/lib/rush/ast/for_node.rb +34 -0
  15. data/lib/rush/ast/function_def.rb +26 -0
  16. data/lib/rush/ast/if_node.rb +29 -0
  17. data/lib/rush/ast/list.rb +47 -0
  18. data/lib/rush/ast/node.rb +17 -0
  19. data/lib/rush/ast/param_ref.rb +59 -0
  20. data/lib/rush/ast/pipeline.rb +45 -0
  21. data/lib/rush/ast/redirect.rb +21 -0
  22. data/lib/rush/ast/redirected.rb +26 -0
  23. data/lib/rush/ast/simple_command.rb +62 -0
  24. data/lib/rush/ast/subshell.rb +24 -0
  25. data/lib/rush/ast/until_node.rb +18 -0
  26. data/lib/rush/ast/while_node.rb +18 -0
  27. data/lib/rush/ast/word.rb +44 -0
  28. data/lib/rush/ast/word_segment.rb +203 -0
  29. data/lib/rush/background_runner.rb +68 -0
  30. data/lib/rush/bracket_expression.rb +107 -0
  31. data/lib/rush/bracket_scanner.rb +64 -0
  32. data/lib/rush/builtins/alias_.rb +74 -0
  33. data/lib/rush/builtins/base.rb +96 -0
  34. data/lib/rush/builtins/break_.rb +19 -0
  35. data/lib/rush/builtins/cd.rb +127 -0
  36. data/lib/rush/builtins/colon.rb +16 -0
  37. data/lib/rush/builtins/command.rb +137 -0
  38. data/lib/rush/builtins/command_options.rb +103 -0
  39. data/lib/rush/builtins/continue_.rb +19 -0
  40. data/lib/rush/builtins/declare.rb +27 -0
  41. data/lib/rush/builtins/defaults.rb +28 -0
  42. data/lib/rush/builtins/dot.rb +52 -0
  43. data/lib/rush/builtins/echo.rb +37 -0
  44. data/lib/rush/builtins/eval.rb +25 -0
  45. data/lib/rush/builtins/exec.rb +54 -0
  46. data/lib/rush/builtins/exit.rb +45 -0
  47. data/lib/rush/builtins/export.rb +20 -0
  48. data/lib/rush/builtins/false_.rb +16 -0
  49. data/lib/rush/builtins/fd_operand.rb +87 -0
  50. data/lib/rush/builtins/getopts.rb +96 -0
  51. data/lib/rush/builtins/hash.rb +64 -0
  52. data/lib/rush/builtins/job_resume.rb +107 -0
  53. data/lib/rush/builtins/jobs.rb +86 -0
  54. data/lib/rush/builtins/kill.rb +152 -0
  55. data/lib/rush/builtins/local.rb +40 -0
  56. data/lib/rush/builtins/loop_jump.rb +45 -0
  57. data/lib/rush/builtins/printf.rb +39 -0
  58. data/lib/rush/builtins/printf_formatter.rb +111 -0
  59. data/lib/rush/builtins/pwd.rb +17 -0
  60. data/lib/rush/builtins/read.rb +93 -0
  61. data/lib/rush/builtins/read_input.rb +146 -0
  62. data/lib/rush/builtins/readonly.rb +20 -0
  63. data/lib/rush/builtins/registry.rb +36 -0
  64. data/lib/rush/builtins/return_.rb +24 -0
  65. data/lib/rush/builtins/set.rb +101 -0
  66. data/lib/rush/builtins/shift.rb +35 -0
  67. data/lib/rush/builtins/test_.rb +48 -0
  68. data/lib/rush/builtins/test_context.rb +24 -0
  69. data/lib/rush/builtins/test_expr.rb +74 -0
  70. data/lib/rush/builtins/test_grammar.rb +117 -0
  71. data/lib/rush/builtins/test_operators.rb +94 -0
  72. data/lib/rush/builtins/test_tokens.rb +69 -0
  73. data/lib/rush/builtins/times.rb +35 -0
  74. data/lib/rush/builtins/trap.rb +85 -0
  75. data/lib/rush/builtins/true_.rb +16 -0
  76. data/lib/rush/builtins/type_.rb +28 -0
  77. data/lib/rush/builtins/ulimit.rb +258 -0
  78. data/lib/rush/builtins/umask.rb +81 -0
  79. data/lib/rush/builtins/unalias.rb +46 -0
  80. data/lib/rush/builtins/unset.rb +40 -0
  81. data/lib/rush/builtins/wait.rb +127 -0
  82. data/lib/rush/cli.rb +31 -0
  83. data/lib/rush/command_assignments.rb +56 -0
  84. data/lib/rush/command_lookup.rb +228 -0
  85. data/lib/rush/command_resolution.rb +115 -0
  86. data/lib/rush/command_runner.rb +152 -0
  87. data/lib/rush/command_text.rb +140 -0
  88. data/lib/rush/environment.rb +134 -0
  89. data/lib/rush/errexit_context.rb +68 -0
  90. data/lib/rush/error_policy.rb +64 -0
  91. data/lib/rush/errors.rb +100 -0
  92. data/lib/rush/escape_table.rb +22 -0
  93. data/lib/rush/escaped_bracket.rb +49 -0
  94. data/lib/rush/executor.rb +164 -0
  95. data/lib/rush/exit_trap.rb +109 -0
  96. data/lib/rush/expansion/arithmetic/evaluator.rb +43 -0
  97. data/lib/rush/expansion/arithmetic/nodes.rb +180 -0
  98. data/lib/rush/expansion/arithmetic/number.rb +104 -0
  99. data/lib/rush/expansion/arithmetic/parser.rb +155 -0
  100. data/lib/rush/expansion/arithmetic/tokenizer.rb +56 -0
  101. data/lib/rush/expansion/arithmetic_expander.rb +38 -0
  102. data/lib/rush/expansion/command_substitution.rb +87 -0
  103. data/lib/rush/expansion/field_part.rb +12 -0
  104. data/lib/rush/expansion/field_splitter.rb +23 -0
  105. data/lib/rush/expansion/glob_expander.rb +48 -0
  106. data/lib/rush/expansion/ifs.rb +62 -0
  107. data/lib/rush/expansion/ifs_scanner.rb +168 -0
  108. data/lib/rush/expansion/parameter_expander.rb +204 -0
  109. data/lib/rush/expansion/parameter_forms.rb +22 -0
  110. data/lib/rush/expansion/pattern_removal.rb +63 -0
  111. data/lib/rush/expansion/pipeline.rb +124 -0
  112. data/lib/rush/expansion/read_char.rb +13 -0
  113. data/lib/rush/expansion/read_field_scanner.rb +159 -0
  114. data/lib/rush/expansion/read_splitter.rb +40 -0
  115. data/lib/rush/expansion/resolver.rb +23 -0
  116. data/lib/rush/expansion/tilde_expander.rb +102 -0
  117. data/lib/rush/external.rb +75 -0
  118. data/lib/rush/fd_entry.rb +60 -0
  119. data/lib/rush/for_runner.rb +50 -0
  120. data/lib/rush/function_frame.rb +30 -0
  121. data/lib/rush/function_runner.rb +37 -0
  122. data/lib/rush/function_table.rb +35 -0
  123. data/lib/rush/getopts_parser.rb +195 -0
  124. data/lib/rush/getopts_state.rb +104 -0
  125. data/lib/rush/here_doc.rb +57 -0
  126. data/lib/rush/interactive_signals.rb +26 -0
  127. data/lib/rush/invocation.rb +190 -0
  128. data/lib/rush/io_table.rb +94 -0
  129. data/lib/rush/job_control.rb +197 -0
  130. data/lib/rush/job_report.rb +45 -0
  131. data/lib/rush/job_spec.rb +51 -0
  132. data/lib/rush/job_table/control.rb +103 -0
  133. data/lib/rush/job_table/interruptible_wait.rb +70 -0
  134. data/lib/rush/job_table/job.rb +181 -0
  135. data/lib/rush/job_table.rb +194 -0
  136. data/lib/rush/lexer/alias_expander.rb +129 -0
  137. data/lib/rush/lexer/braced_reader.rb +86 -0
  138. data/lib/rush/lexer/case_frame.rb +114 -0
  139. data/lib/rush/lexer/case_tracker.rb +146 -0
  140. data/lib/rush/lexer/dollar_scanner.rb +53 -0
  141. data/lib/rush/lexer/double_quote_scanner.rb +71 -0
  142. data/lib/rush/lexer/heredoc_body.rb +91 -0
  143. data/lib/rush/lexer/heredoc_reader.rb +93 -0
  144. data/lib/rush/lexer/lex_state.rb +128 -0
  145. data/lib/rush/lexer/operator_table.rb +32 -0
  146. data/lib/rush/lexer/param_scanner.rb +43 -0
  147. data/lib/rush/lexer/paren_reader.rb +113 -0
  148. data/lib/rush/lexer/paren_regions.rb +74 -0
  149. data/lib/rush/lexer/quote_skips.rb +77 -0
  150. data/lib/rush/lexer/quoted_word.rb +67 -0
  151. data/lib/rush/lexer/scanner_predicates.rb +20 -0
  152. data/lib/rush/lexer/source_lines.rb +43 -0
  153. data/lib/rush/lexer/substitution_reader.rb +77 -0
  154. data/lib/rush/lexer/token_classifier.rb +144 -0
  155. data/lib/rush/lexer/token_predicates.rb +23 -0
  156. data/lib/rush/lexer/word_scanner.rb +134 -0
  157. data/lib/rush/lexer.rb +174 -0
  158. data/lib/rush/loop_control_handling.rb +29 -0
  159. data/lib/rush/loop_nesting.rb +50 -0
  160. data/lib/rush/loop_runner.rb +55 -0
  161. data/lib/rush/option_cluster.rb +39 -0
  162. data/lib/rush/options.rb +76 -0
  163. data/lib/rush/param_text.rb +39 -0
  164. data/lib/rush/parser.rb +1213 -0
  165. data/lib/rush/parser_support.rb +112 -0
  166. data/lib/rush/pattern_scanner.rb +35 -0
  167. data/lib/rush/pending_signals.rb +57 -0
  168. data/lib/rush/pipeline_runner.rb +173 -0
  169. data/lib/rush/pipeline_statuses.rb +45 -0
  170. data/lib/rush/positional.rb +45 -0
  171. data/lib/rush/posix_pattern.rb +78 -0
  172. data/lib/rush/program_input.rb +89 -0
  173. data/lib/rush/program_reader.rb +69 -0
  174. data/lib/rush/program_session.rb +83 -0
  175. data/lib/rush/prompt.rb +52 -0
  176. data/lib/rush/redirect_scope.rb +64 -0
  177. data/lib/rush/redirection/dup_redirect.rb +58 -0
  178. data/lib/rush/redirection/file_redirect.rb +50 -0
  179. data/lib/rush/redirection/here_doc_redirect.rb +17 -0
  180. data/lib/rush/redirection/registry.rb +53 -0
  181. data/lib/rush/repl.rb +133 -0
  182. data/lib/rush/runtime_type_checks.rb +16 -0
  183. data/lib/rush/scope.rb +79 -0
  184. data/lib/rush/segment_buffer.rb +47 -0
  185. data/lib/rush/shell_parameters.rb +77 -0
  186. data/lib/rush/shell_pattern.rb +80 -0
  187. data/lib/rush/shell_state.rb +163 -0
  188. data/lib/rush/shell_variables.rb +67 -0
  189. data/lib/rush/signal_report.rb +41 -0
  190. data/lib/rush/signals.rb +68 -0
  191. data/lib/rush/source.rb +82 -0
  192. data/lib/rush/source_line_counter.rb +41 -0
  193. data/lib/rush/source_runner.rb +47 -0
  194. data/lib/rush/startup.rb +75 -0
  195. data/lib/rush/status.rb +93 -0
  196. data/lib/rush/stop_relay.rb +32 -0
  197. data/lib/rush/subshell_runner.rb +70 -0
  198. data/lib/rush/system_calls/collation.rb +155 -0
  199. data/lib/rush/system_calls/file_tests.rb +95 -0
  200. data/lib/rush/system_calls/process_control.rb +165 -0
  201. data/lib/rush/system_calls/process_identity.rb +43 -0
  202. data/lib/rush/system_calls/resource_limits.rb +51 -0
  203. data/lib/rush/system_calls.rb +206 -0
  204. data/lib/rush/terminal.rb +126 -0
  205. data/lib/rush/trap_runner.rb +169 -0
  206. data/lib/rush/trap_table.rb +49 -0
  207. data/lib/rush/umask_mode.rb +133 -0
  208. data/lib/rush/version.rb +6 -0
  209. data/lib/rush.rb +190 -0
  210. metadata +527 -0
@@ -0,0 +1,109 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ # Owns the one-shot EXIT-trap lifecycle, terminating-status context and fatal
6
+ # error boundary. Ordinary delivered traps use TrapRunner's recoverable policy;
7
+ # EXIT runs at shell teardown and therefore converts fatal shell errors to 2.
8
+ class ExitTrap
9
+ extend T::Sig
10
+
11
+ # Trap source paired with the shell status being terminated.
12
+ Execution = Data.define(:source, :code)
13
+
14
+ sig { params(executor: Executor).void }
15
+ def initialize(executor)
16
+ @executor = executor
17
+ @state = executor.state
18
+ @exiting = T.let(nil, T.nilable(Integer))
19
+ @fired = false
20
+ end
21
+
22
+ sig { params(code: Integer).returns(Integer) }
23
+ def run(code)
24
+ action = claim_action
25
+ return code unless action
26
+
27
+ @state.record_status(Status.new(code))
28
+ fire(Execution.new(action, code))
29
+ end
30
+
31
+ sig { returns(Integer) }
32
+ def status
33
+ @exiting || @state.last_status.exitstatus
34
+ end
35
+
36
+ private
37
+
38
+ sig { returns(T.nilable(String)) }
39
+ def claim_action
40
+ return if @fired
41
+
42
+ @fired = true
43
+ @state.traps.action(Signals::EXIT)
44
+ end
45
+
46
+ sig { params(execution: Execution).returns(Integer) }
47
+ def fire(execution)
48
+ exit_result(execution)
49
+ rescue Error => e
50
+ raise unless decision(e) == :abort2
51
+
52
+ abort(e)
53
+ end
54
+
55
+ sig { params(execution: Execution).returns(Integer) }
56
+ def exit_result(execution)
57
+ control_result(execution)
58
+ rescue Error => e
59
+ return e.code if decision(e) == :override_code && e.is_a?(ExitSignal)
60
+
61
+ raise
62
+ end
63
+
64
+ sig { params(execution: Execution).returns(Integer) }
65
+ def control_result(execution)
66
+ run_execution(execution)
67
+ rescue Error => e
68
+ return execution.code if decision(e) == :preserve_code
69
+
70
+ raise
71
+ end
72
+
73
+ sig { params(execution: Execution).returns(Integer) }
74
+ def run_execution(execution)
75
+ source, code = execution.deconstruct
76
+ with_status(code) { evaluate(source) }
77
+ code
78
+ end
79
+
80
+ sig { params(error: Error).returns(Symbol) }
81
+ def decision(error)
82
+ ErrorPolicy.decision(:exit_trap, error)
83
+ end
84
+
85
+ sig { params(error: Error).returns(Integer) }
86
+ def abort(error)
87
+ @executor.system.stderr.puts("rush: #{error.message}")
88
+ @state.record_status(Status.new(2))
89
+ 2
90
+ end
91
+
92
+ sig do
93
+ type_parameters(:U)
94
+ .params(code: Integer, blk: T.proc.returns(T.type_parameter(:U)))
95
+ .returns(T.type_parameter(:U))
96
+ end
97
+ def with_status(code, &blk)
98
+ @exiting = code
99
+ yield
100
+ ensure
101
+ @exiting = nil
102
+ end
103
+
104
+ sig { params(action: String).void }
105
+ def evaluate(action)
106
+ @executor.run(Parser.new(Lexer.new(action, aliases: @state.aliases)).parse)
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,43 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Expansion
6
+ module Arithmetic
7
+ # Evaluates an already parameter-expanded arithmetic string: tokenize,
8
+ # parse, walk the AST. A bare name resolves to its shell value parsed as an
9
+ # integer constant (unset or blank is 0); a non-numeric value is an error,
10
+ # as in dash. Recursion into a value as an *expression* is not done.
11
+ class Evaluator
12
+ extend T::Sig
13
+
14
+ sig { params(executor: Executor).void }
15
+ def initialize(executor)
16
+ @executor = executor
17
+ end
18
+
19
+ sig { params(source: String).returns(Integer) }
20
+ def evaluate(source)
21
+ Parser.new(Tokenizer.new(source).tokens).parse.result(self)
22
+ end
23
+
24
+ sig { params(name: String).returns(Integer) }
25
+ def resolve(name)
26
+ value = @executor.state.variables.get(name) || ''
27
+ return 0 if value.strip.empty?
28
+
29
+ Number.parse(value)
30
+ end
31
+
32
+ # `name op= value`: for a compound operator, combine with the current
33
+ # value first (`+=` -> `+`); store the wrapped result and return it.
34
+ sig { params(name: String, op: String, value: Integer).returns(Integer) }
35
+ def assign(name, op, value)
36
+ result = op == '=' ? value : Number.binary(op.chop, resolve(name), value)
37
+ @executor.state.variables.assign(name, result.to_s)
38
+ result
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,180 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Expansion
6
+ module Arithmetic
7
+ # The arithmetic AST. Each node computes against an Evaluator (`ctx`), which
8
+ # resolves variable names. And/Or and the conditional short-circuit so a
9
+ # branch that is not taken never runs (matching dash, where `0 && 1/0` is 0).
10
+ #
11
+ # The node payloads are ordinary typed Ruby state rather than generated
12
+ # reader methods: both Steep (via RBS) and Sorbet (via inline sigs) can see
13
+ # the operator/name/child invariants carried through parser and evaluator.
14
+ Node = T.type_alias { T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign) }
15
+
16
+ # A literal integer.
17
+ class Num
18
+ extend T::Sig
19
+
20
+ sig { returns(Integer) }
21
+ attr_reader :value
22
+
23
+ sig { params(value: Integer).void }
24
+ def initialize(value)
25
+ @value = value
26
+ end
27
+
28
+ sig { params(_ctx: Evaluator).returns(Integer) }
29
+ def result(_ctx)
30
+ value
31
+ end
32
+ end
33
+
34
+ # A variable reference; resolves its name through the evaluator.
35
+ class Var
36
+ extend T::Sig
37
+
38
+ sig { returns(String) }
39
+ attr_reader :name
40
+
41
+ sig { params(name: String).void }
42
+ def initialize(name)
43
+ @name = name
44
+ end
45
+
46
+ sig { params(ctx: Evaluator).returns(Integer) }
47
+ def result(ctx)
48
+ ctx.resolve(name)
49
+ end
50
+ end
51
+
52
+ # A unary operation (+ - ! ~) on one operand.
53
+ class Unary
54
+ extend T::Sig
55
+
56
+ sig { returns(String) }
57
+ attr_reader :op
58
+
59
+ sig { returns(Node) }
60
+ attr_reader :operand
61
+
62
+ sig { params(op: String, operand: Node).void }
63
+ def initialize(op, operand)
64
+ @op = op
65
+ @operand = operand
66
+ end
67
+
68
+ sig { params(ctx: Evaluator).returns(Integer) }
69
+ def result(ctx)
70
+ Number.unary(op, operand.result(ctx))
71
+ end
72
+ end
73
+
74
+ # A binary arithmetic / bitwise / comparison operation on two operands.
75
+ class Binary
76
+ extend T::Sig
77
+
78
+ sig { returns(String) }
79
+ attr_reader :op
80
+
81
+ sig { returns(Node) }
82
+ attr_reader :left, :right
83
+
84
+ sig { params(op: String, left: Node, right: Node).void }
85
+ def initialize(op, left, right)
86
+ @op = op
87
+ @left = left
88
+ @right = right
89
+ end
90
+
91
+ sig { params(ctx: Evaluator).returns(Integer) }
92
+ def result(ctx)
93
+ Number.binary(op, left.result(ctx), right.result(ctx))
94
+ end
95
+ end
96
+
97
+ # Logical &&: short-circuits, so the right operand runs only when the left is non-zero.
98
+ class And
99
+ extend T::Sig
100
+
101
+ sig { returns(Node) }
102
+ attr_reader :left, :right
103
+
104
+ sig { params(left: Node, right: Node).void }
105
+ def initialize(left, right)
106
+ @left = left
107
+ @right = right
108
+ end
109
+
110
+ sig { params(ctx: Evaluator).returns(Integer) }
111
+ def result(ctx)
112
+ left.result(ctx).zero? ? 0 : Number.bool(!right.result(ctx).zero?)
113
+ end
114
+ end
115
+
116
+ # Logical ||: short-circuits, so the right operand runs only when the left is zero.
117
+ class Or
118
+ extend T::Sig
119
+
120
+ sig { returns(Node) }
121
+ attr_reader :left, :right
122
+
123
+ sig { params(left: Node, right: Node).void }
124
+ def initialize(left, right)
125
+ @left = left
126
+ @right = right
127
+ end
128
+
129
+ sig { params(ctx: Evaluator).returns(Integer) }
130
+ def result(ctx)
131
+ left.result(ctx).zero? ? Number.bool(!right.result(ctx).zero?) : 1
132
+ end
133
+ end
134
+
135
+ # The ?: conditional: only the taken branch is evaluated.
136
+ class Cond
137
+ extend T::Sig
138
+
139
+ sig { returns(Node) }
140
+ attr_reader :test, :truthy, :falsy
141
+
142
+ sig { params(test: Node, truthy: Node, falsy: Node).void }
143
+ def initialize(test, truthy, falsy)
144
+ @test = test
145
+ @truthy = truthy
146
+ @falsy = falsy
147
+ end
148
+
149
+ sig { params(ctx: Evaluator).returns(Integer) }
150
+ def result(ctx)
151
+ test.result(ctx).zero? ? falsy.result(ctx) : truthy.result(ctx)
152
+ end
153
+ end
154
+
155
+ # The right-hand side is evaluated before the target is read, so a nested
156
+ # assignment in the rhs (e.g. `a += a += 1`) takes effect first.
157
+ class Assign
158
+ extend T::Sig
159
+
160
+ sig { returns(String) }
161
+ attr_reader :name, :op
162
+
163
+ sig { returns(Node) }
164
+ attr_reader :rhs
165
+
166
+ sig { params(name: String, op: String, rhs: Node).void }
167
+ def initialize(name, op, rhs)
168
+ @name = name
169
+ @op = op
170
+ @rhs = rhs
171
+ end
172
+
173
+ sig { params(ctx: Evaluator).returns(Integer) }
174
+ def result(ctx)
175
+ ctx.assign(name, op, rhs.result(ctx))
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,104 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Expansion
6
+ module Arithmetic
7
+ # Pure 64-bit integer arithmetic for $(( )): two's-complement wrapping (so
8
+ # overflow matches dash), C-style truncated-toward-zero division/modulo,
9
+ # and the unary/binary operator tables the evaluator dispatches through.
10
+ # Comparison and logical results are 1 or 0. Never uses Kernel#eval.
11
+ module Number
12
+ extend T::Sig
13
+
14
+ module_function
15
+
16
+ LIMIT = 1 << 63
17
+
18
+ # Parse an integer constant (decimal, 0-octal or 0x-hex, optional sign and
19
+ # surrounding blanks), as both literals and variable values are read. The
20
+ # raising Integer() (not the exception:false form) lets the failure path
21
+ # translate to an ExpansionError; it also types cleanly under both checkers
22
+ # (Sorbet's RBI mistypes Integer(exception: false) as non-nil). Kernel.* so
23
+ # the bare Kernel calls resolve in this module_function module under Sorbet.
24
+ sig { params(text: String).returns(Integer) }
25
+ def parse(text)
26
+ Kernel.Integer(text)
27
+ rescue ArgumentError
28
+ Kernel.raise(ExpansionError, "arithmetic: invalid number #{text.inspect}")
29
+ end
30
+
31
+ # op is String here, the literal operator set ("+"|"-"|"!"|"~") in the RBS
32
+ # sig: Sorbet has no string-literal types, so String is the strongest it can
33
+ # say. Steep reaches the union because Parser#unary narrows the token via the
34
+ # typed UNARY table (Array[unary_op]) — a materialised drift, see number.rbs.
35
+ sig { params(op: String, value: Integer).returns(Integer) }
36
+ def unary(op, value)
37
+ wrap(UNARY.fetch(op).call(value))
38
+ end
39
+
40
+ sig { params(op: String, left: Integer, right: Integer).returns(Integer) }
41
+ def binary(op, left, right)
42
+ wrap(BINARY.fetch(op).call(left, right))
43
+ end
44
+
45
+ sig { params(num: Integer).returns(Integer) }
46
+ def wrap(num)
47
+ ((num + LIMIT) % (LIMIT << 1)) - LIMIT
48
+ end
49
+
50
+ sig { params(flag: T::Boolean).returns(Integer) }
51
+ def bool(flag)
52
+ flag ? 1 : 0
53
+ end
54
+
55
+ sig { params(left: Integer, right: Integer).returns(Integer) }
56
+ def divide(left, right)
57
+ Kernel.raise(ExpansionError, 'arithmetic: division by zero') if right.zero?
58
+
59
+ magnitude = left.abs / right.abs
60
+ left.negative? == right.negative? ? magnitude : -magnitude
61
+ end
62
+
63
+ sig { params(left: Integer, right: Integer).returns(Integer) }
64
+ def modulo(left, right)
65
+ left - (divide(left, right) * right)
66
+ end
67
+
68
+ # The #: comments type each lambda for Steep; the T.let around the whole
69
+ # table gives Sorbet the same proc shape without hiding the lambda from
70
+ # Steep's comment parser.
71
+ UNARY_PROC = T.type_alias { T.proc.params(value: Integer).returns(Integer) }
72
+ BINARY_PROC = T.type_alias { T.proc.params(left: Integer, right: Integer).returns(Integer) }
73
+
74
+ UNARY = T.let({
75
+ '+' => ->(value) { T.let(value, Integer) }, #: ^(Integer) -> Integer
76
+ '-' => ->(value) { -T.let(value, Integer) }, #: ^(Integer) -> Integer
77
+ '!' => ->(value) { bool(T.let(value, Integer).zero?) }, #: ^(Integer) -> Integer
78
+ '~' => ->(value) { ~T.let(value, Integer) } #: ^(Integer) -> Integer
79
+ }.freeze, T::Hash[String, UNARY_PROC])
80
+
81
+ BINARY = T.let({
82
+ '+' => ->(left, right) { T.let(left, Integer) + T.let(right, Integer) }, #: ^(Integer, Integer) -> Integer
83
+ '-' => ->(left, right) { T.let(left, Integer) - T.let(right, Integer) }, #: ^(Integer, Integer) -> Integer
84
+ '*' => ->(left, right) { T.let(left, Integer) * T.let(right, Integer) }, #: ^(Integer, Integer) -> Integer
85
+ '/' => ->(left, right) { divide(T.let(left, Integer), T.let(right, Integer)) }, #: ^(Integer, Integer) -> Integer
86
+ '%' => ->(left, right) { modulo(T.let(left, Integer), T.let(right, Integer)) }, #: ^(Integer, Integer) -> Integer
87
+ # Shift counts are masked to 6 bits, matching x86-64 (and so dash) for
88
+ # the out-of-range/negative counts that C leaves undefined.
89
+ '<<' => ->(left, right) { T.let(left, Integer) << (T.let(right, Integer) & 63) }, #: ^(Integer, Integer) -> Integer
90
+ '>>' => ->(left, right) { T.let(left, Integer) >> (T.let(right, Integer) & 63) }, #: ^(Integer, Integer) -> Integer
91
+ '&' => ->(left, right) { T.let(left, Integer) & T.let(right, Integer) }, #: ^(Integer, Integer) -> Integer
92
+ '|' => ->(left, right) { T.let(left, Integer) | T.let(right, Integer) }, #: ^(Integer, Integer) -> Integer
93
+ '^' => ->(left, right) { T.let(left, Integer) ^ T.let(right, Integer) }, #: ^(Integer, Integer) -> Integer
94
+ '<' => ->(left, right) { bool(T.let(left, Integer) < T.let(right, Integer)) }, #: ^(Integer, Integer) -> Integer
95
+ '<=' => ->(left, right) { bool(T.let(left, Integer) <= T.let(right, Integer)) }, #: ^(Integer, Integer) -> Integer
96
+ '>' => ->(left, right) { bool(T.let(left, Integer) > T.let(right, Integer)) }, #: ^(Integer, Integer) -> Integer
97
+ '>=' => ->(left, right) { bool(T.let(left, Integer) >= T.let(right, Integer)) }, #: ^(Integer, Integer) -> Integer
98
+ '==' => ->(left, right) { bool(T.let(left, Integer) == T.let(right, Integer)) }, #: ^(Integer, Integer) -> Integer
99
+ '!=' => ->(left, right) { bool(T.let(left, Integer) != T.let(right, Integer)) } #: ^(Integer, Integer) -> Integer
100
+ }.freeze, T::Hash[String, BINARY_PROC])
101
+ end
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,155 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ module Rush
5
+ module Expansion
6
+ module Arithmetic
7
+ # A Pratt (precedence-climbing) parser turning the tokens into the AST.
8
+ # Binary precedence is table-driven; the conditional operator (?:) binds
9
+ # loosest and is right-associative. Malformed input or leftover tokens
10
+ # raise a syntax error (matching dash's "expecting primary"/"expecting EOF").
11
+ class Parser
12
+ extend T::Sig
13
+
14
+ PRECEDENCE = {
15
+ '||' => 1, '&&' => 2, '|' => 3, '^' => 4, '&' => 5,
16
+ '==' => 6, '!=' => 6, '<' => 7, '<=' => 7, '>' => 7, '>=' => 7,
17
+ '<<' => 8, '>>' => 8, '+' => 9, '-' => 9, '*' => 10, '/' => 10, '%' => 10
18
+ }.freeze
19
+ UNARY = %w[+ - ! ~].freeze
20
+ ASSIGN = ['=', '+=', '-=', '*=', '/=', '%=', '<<=', '>>=', '&=', '^=', '|='].freeze
21
+
22
+ sig { params(tokens: T::Array[[Symbol, String]]).void }
23
+ def initialize(tokens)
24
+ @tokens = tokens
25
+ @pos = 0
26
+ end
27
+
28
+ sig { returns(T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign)) }
29
+ def parse
30
+ node = assignment
31
+ oops unless @pos == @tokens.size
32
+ node
33
+ end
34
+
35
+ private
36
+
37
+ # Assignment binds loosest and is right-associative; its target must be a
38
+ # bare name (an lvalue), so e.g. `5 = 3` is a syntax error.
39
+ sig { returns(T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign)) }
40
+ def assignment
41
+ left = conditional
42
+ return left unless ASSIGN.include?(peek)
43
+
44
+ oops unless left.is_a?(Var)
45
+ Assign.new(left.name, advance, assignment)
46
+ end
47
+
48
+ sig { returns(T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign)) }
49
+ def conditional
50
+ test = binary(1)
51
+ return test unless accept?('?')
52
+
53
+ branch = assignment
54
+ expect(':')
55
+ Cond.new(test, branch, conditional)
56
+ end
57
+
58
+ sig { params(min: Integer).returns(T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign)) }
59
+ def binary(min)
60
+ left = unary
61
+ left = fold(left) while (tok = peek) && (power = PRECEDENCE.fetch(tok, nil)) && power >= min
62
+ left
63
+ end
64
+
65
+ sig do
66
+ params(left: T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign))
67
+ .returns(T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign))
68
+ end
69
+ def fold(left)
70
+ op = advance
71
+ combine(op, left, binary(PRECEDENCE.fetch(op) + 1))
72
+ end
73
+
74
+ sig do
75
+ params(op: String,
76
+ left: T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign),
77
+ right: T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign))
78
+ .returns(T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign))
79
+ end
80
+ def combine(op, left, right)
81
+ return And.new(left, right) if op == '&&'
82
+ return Or.new(left, right) if op == '||'
83
+
84
+ Binary.new(op, left, right)
85
+ end
86
+
87
+ sig { returns(T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign)) }
88
+ def unary
89
+ # `find` (not `include?`) hands back the matching UNARY element, so under
90
+ # Steep — where UNARY is Array[unary_op] — `op` carries the literal
91
+ # operator type into Unary.new instead of a bare scanned String. This is
92
+ # the narrowing point that makes the RBS literal union reachable; Sorbet,
93
+ # with no string-literal types, still sees String. See docs/journal.md.
94
+ token = peek
95
+ op = UNARY.find { |candidate| candidate == token }
96
+ return primary unless op
97
+
98
+ advance
99
+ Unary.new(op, unary)
100
+ end
101
+
102
+ sig { returns(T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign)) }
103
+ def primary
104
+ return grouped if accept?('(')
105
+
106
+ kind, text = take
107
+ return Num.new(Number.parse(text)) if kind == :num
108
+
109
+ kind == :name ? Var.new(text) : oops
110
+ end
111
+
112
+ sig { returns(T.any(Num, Var, Unary, Binary, And, Or, Cond, Assign)) }
113
+ def grouped
114
+ node = assignment
115
+ expect(')')
116
+ node
117
+ end
118
+
119
+ sig { returns(T.nilable(String)) }
120
+ def peek
121
+ @tokens[@pos]&.last
122
+ end
123
+
124
+ sig { returns(String) }
125
+ def advance
126
+ take.last
127
+ end
128
+
129
+ sig { returns([Symbol, String]) }
130
+ def take
131
+ oops if @pos >= @tokens.size
132
+ @tokens.fetch(@pos).tap { @pos += 1 }
133
+ end
134
+
135
+ sig { params(token: String).returns(T::Boolean) }
136
+ def accept?(token)
137
+ return false unless peek == token
138
+
139
+ @pos += 1
140
+ true
141
+ end
142
+
143
+ sig { params(token: String).void }
144
+ def expect(token)
145
+ accept?(token) || oops
146
+ end
147
+
148
+ sig { returns(T.noreturn) }
149
+ def oops
150
+ raise(ExpansionError, 'arithmetic: syntax error')
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,56 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'strscan'
5
+
6
+ module Rush
7
+ module Expansion
8
+ module Arithmetic
9
+ # Splits an arithmetic expression into [kind, text] tokens (:num, :name,
10
+ # :op), skipping blanks. Operators are listed longest-first so multi-byte
11
+ # ones win over their prefixes; any other leftover character is an error.
12
+ class Tokenizer
13
+ extend T::Sig
14
+
15
+ OPERATORS = [
16
+ '<<=', '>>=',
17
+ '<<', '>>', '<=', '>=', '==', '!=', '&&', '||', '+=', '-=', '*=', '/=', '%=', '&=', '^=', '|=',
18
+ '+', '-', '*', '/', '%', '<', '>', '&', '^', '|', '~', '!', '(', ')', '?', ':', '='
19
+ ].freeze
20
+ OPERATOR = Regexp.union(OPERATORS)
21
+ NUMBER = /0[xX][0-9a-fA-F]+|\d+/
22
+ NAME = /[A-Za-z_]\w*/
23
+ TOKENS = { num: NUMBER, name: NAME, op: OPERATOR }.freeze
24
+
25
+ sig { params(source: String).void }
26
+ def initialize(source)
27
+ @scanner = StringScanner.new(source)
28
+ end
29
+
30
+ sig { returns(T::Array[[Symbol, String]]) }
31
+ def tokens
32
+ result = [] #: Array[[Symbol, String]]
33
+ result << next_token while at_token?
34
+ result
35
+ end
36
+
37
+ private
38
+
39
+ sig { returns(T::Boolean) }
40
+ def at_token?
41
+ @scanner.skip(/\s+/)
42
+ !@scanner.eos?
43
+ end
44
+
45
+ sig { returns([Symbol, String]) }
46
+ def next_token
47
+ TOKENS.lazy
48
+ .map { |kind, pattern| [kind, @scanner.scan(pattern)] }
49
+ .find(
50
+ -> { raise ExpansionError, "arithmetic: unexpected #{@scanner.rest.inspect}" }
51
+ ) { |_kind, found| found }
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,38 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require 'strscan'
5
+
6
+ module Rush
7
+ module Expansion
8
+ # Expands $(( ... )): first apply parameter expansion and command
9
+ # substitution to the raw text (POSIX treats it like a double-quoted word),
10
+ # then evaluate the result as an integer arithmetic expression.
11
+ class ArithmeticExpander
12
+ extend T::Sig
13
+
14
+ sig { params(executor: Executor, source: String).void }
15
+ def initialize(executor, source)
16
+ @executor = executor
17
+ @source = source
18
+ end
19
+
20
+ sig { returns(String) }
21
+ def expand
22
+ Arithmetic::Evaluator.new(@executor).evaluate(expanded).to_s
23
+ end
24
+
25
+ private
26
+
27
+ sig { returns(String) }
28
+ def expanded
29
+ @executor.expander.expand_value(scanned, tilde: :none)
30
+ end
31
+
32
+ sig { returns(AST::Word) }
33
+ def scanned
34
+ Lexer::WordScanner.entire(@source)
35
+ end
36
+ end
37
+ end
38
+ end