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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +94 -0
- data/exe/rush +10 -0
- data/grammar/shell.y +272 -0
- data/grammar/shell.y.output +2354 -0
- data/lib/rush/alias_table.rb +48 -0
- data/lib/rush/assignments.rb +48 -0
- data/lib/rush/ast/and_or.rb +37 -0
- data/lib/rush/ast/assignment.rb +20 -0
- data/lib/rush/ast/brace_group.rb +25 -0
- data/lib/rush/ast/case_node.rb +41 -0
- data/lib/rush/ast/condition_loop.rb +34 -0
- data/lib/rush/ast/for_node.rb +34 -0
- data/lib/rush/ast/function_def.rb +26 -0
- data/lib/rush/ast/if_node.rb +29 -0
- data/lib/rush/ast/list.rb +47 -0
- data/lib/rush/ast/node.rb +17 -0
- data/lib/rush/ast/param_ref.rb +59 -0
- data/lib/rush/ast/pipeline.rb +45 -0
- data/lib/rush/ast/redirect.rb +21 -0
- data/lib/rush/ast/redirected.rb +26 -0
- data/lib/rush/ast/simple_command.rb +62 -0
- data/lib/rush/ast/subshell.rb +24 -0
- data/lib/rush/ast/until_node.rb +18 -0
- data/lib/rush/ast/while_node.rb +18 -0
- data/lib/rush/ast/word.rb +44 -0
- data/lib/rush/ast/word_segment.rb +203 -0
- data/lib/rush/background_runner.rb +68 -0
- data/lib/rush/bracket_expression.rb +107 -0
- data/lib/rush/bracket_scanner.rb +64 -0
- data/lib/rush/builtins/alias_.rb +74 -0
- data/lib/rush/builtins/base.rb +96 -0
- data/lib/rush/builtins/break_.rb +19 -0
- data/lib/rush/builtins/cd.rb +127 -0
- data/lib/rush/builtins/colon.rb +16 -0
- data/lib/rush/builtins/command.rb +137 -0
- data/lib/rush/builtins/command_options.rb +103 -0
- data/lib/rush/builtins/continue_.rb +19 -0
- data/lib/rush/builtins/declare.rb +27 -0
- data/lib/rush/builtins/defaults.rb +28 -0
- data/lib/rush/builtins/dot.rb +52 -0
- data/lib/rush/builtins/echo.rb +37 -0
- data/lib/rush/builtins/eval.rb +25 -0
- data/lib/rush/builtins/exec.rb +54 -0
- data/lib/rush/builtins/exit.rb +45 -0
- data/lib/rush/builtins/export.rb +20 -0
- data/lib/rush/builtins/false_.rb +16 -0
- data/lib/rush/builtins/fd_operand.rb +87 -0
- data/lib/rush/builtins/getopts.rb +96 -0
- data/lib/rush/builtins/hash.rb +64 -0
- data/lib/rush/builtins/job_resume.rb +107 -0
- data/lib/rush/builtins/jobs.rb +86 -0
- data/lib/rush/builtins/kill.rb +152 -0
- data/lib/rush/builtins/local.rb +40 -0
- data/lib/rush/builtins/loop_jump.rb +45 -0
- data/lib/rush/builtins/printf.rb +39 -0
- data/lib/rush/builtins/printf_formatter.rb +111 -0
- data/lib/rush/builtins/pwd.rb +17 -0
- data/lib/rush/builtins/read.rb +93 -0
- data/lib/rush/builtins/read_input.rb +146 -0
- data/lib/rush/builtins/readonly.rb +20 -0
- data/lib/rush/builtins/registry.rb +36 -0
- data/lib/rush/builtins/return_.rb +24 -0
- data/lib/rush/builtins/set.rb +101 -0
- data/lib/rush/builtins/shift.rb +35 -0
- data/lib/rush/builtins/test_.rb +48 -0
- data/lib/rush/builtins/test_context.rb +24 -0
- data/lib/rush/builtins/test_expr.rb +74 -0
- data/lib/rush/builtins/test_grammar.rb +117 -0
- data/lib/rush/builtins/test_operators.rb +94 -0
- data/lib/rush/builtins/test_tokens.rb +69 -0
- data/lib/rush/builtins/times.rb +35 -0
- data/lib/rush/builtins/trap.rb +85 -0
- data/lib/rush/builtins/true_.rb +16 -0
- data/lib/rush/builtins/type_.rb +28 -0
- data/lib/rush/builtins/ulimit.rb +258 -0
- data/lib/rush/builtins/umask.rb +81 -0
- data/lib/rush/builtins/unalias.rb +46 -0
- data/lib/rush/builtins/unset.rb +40 -0
- data/lib/rush/builtins/wait.rb +127 -0
- data/lib/rush/cli.rb +31 -0
- data/lib/rush/command_assignments.rb +56 -0
- data/lib/rush/command_lookup.rb +228 -0
- data/lib/rush/command_resolution.rb +115 -0
- data/lib/rush/command_runner.rb +152 -0
- data/lib/rush/command_text.rb +140 -0
- data/lib/rush/environment.rb +134 -0
- data/lib/rush/errexit_context.rb +68 -0
- data/lib/rush/error_policy.rb +64 -0
- data/lib/rush/errors.rb +100 -0
- data/lib/rush/escape_table.rb +22 -0
- data/lib/rush/escaped_bracket.rb +49 -0
- data/lib/rush/executor.rb +164 -0
- data/lib/rush/exit_trap.rb +109 -0
- data/lib/rush/expansion/arithmetic/evaluator.rb +43 -0
- data/lib/rush/expansion/arithmetic/nodes.rb +180 -0
- data/lib/rush/expansion/arithmetic/number.rb +104 -0
- data/lib/rush/expansion/arithmetic/parser.rb +155 -0
- data/lib/rush/expansion/arithmetic/tokenizer.rb +56 -0
- data/lib/rush/expansion/arithmetic_expander.rb +38 -0
- data/lib/rush/expansion/command_substitution.rb +87 -0
- data/lib/rush/expansion/field_part.rb +12 -0
- data/lib/rush/expansion/field_splitter.rb +23 -0
- data/lib/rush/expansion/glob_expander.rb +48 -0
- data/lib/rush/expansion/ifs.rb +62 -0
- data/lib/rush/expansion/ifs_scanner.rb +168 -0
- data/lib/rush/expansion/parameter_expander.rb +204 -0
- data/lib/rush/expansion/parameter_forms.rb +22 -0
- data/lib/rush/expansion/pattern_removal.rb +63 -0
- data/lib/rush/expansion/pipeline.rb +124 -0
- data/lib/rush/expansion/read_char.rb +13 -0
- data/lib/rush/expansion/read_field_scanner.rb +159 -0
- data/lib/rush/expansion/read_splitter.rb +40 -0
- data/lib/rush/expansion/resolver.rb +23 -0
- data/lib/rush/expansion/tilde_expander.rb +102 -0
- data/lib/rush/external.rb +75 -0
- data/lib/rush/fd_entry.rb +60 -0
- data/lib/rush/for_runner.rb +50 -0
- data/lib/rush/function_frame.rb +30 -0
- data/lib/rush/function_runner.rb +37 -0
- data/lib/rush/function_table.rb +35 -0
- data/lib/rush/getopts_parser.rb +195 -0
- data/lib/rush/getopts_state.rb +104 -0
- data/lib/rush/here_doc.rb +57 -0
- data/lib/rush/interactive_signals.rb +26 -0
- data/lib/rush/invocation.rb +190 -0
- data/lib/rush/io_table.rb +94 -0
- data/lib/rush/job_control.rb +197 -0
- data/lib/rush/job_report.rb +45 -0
- data/lib/rush/job_spec.rb +51 -0
- data/lib/rush/job_table/control.rb +103 -0
- data/lib/rush/job_table/interruptible_wait.rb +70 -0
- data/lib/rush/job_table/job.rb +181 -0
- data/lib/rush/job_table.rb +194 -0
- data/lib/rush/lexer/alias_expander.rb +129 -0
- data/lib/rush/lexer/braced_reader.rb +86 -0
- data/lib/rush/lexer/case_frame.rb +114 -0
- data/lib/rush/lexer/case_tracker.rb +146 -0
- data/lib/rush/lexer/dollar_scanner.rb +53 -0
- data/lib/rush/lexer/double_quote_scanner.rb +71 -0
- data/lib/rush/lexer/heredoc_body.rb +91 -0
- data/lib/rush/lexer/heredoc_reader.rb +93 -0
- data/lib/rush/lexer/lex_state.rb +128 -0
- data/lib/rush/lexer/operator_table.rb +32 -0
- data/lib/rush/lexer/param_scanner.rb +43 -0
- data/lib/rush/lexer/paren_reader.rb +113 -0
- data/lib/rush/lexer/paren_regions.rb +74 -0
- data/lib/rush/lexer/quote_skips.rb +77 -0
- data/lib/rush/lexer/quoted_word.rb +67 -0
- data/lib/rush/lexer/scanner_predicates.rb +20 -0
- data/lib/rush/lexer/source_lines.rb +43 -0
- data/lib/rush/lexer/substitution_reader.rb +77 -0
- data/lib/rush/lexer/token_classifier.rb +144 -0
- data/lib/rush/lexer/token_predicates.rb +23 -0
- data/lib/rush/lexer/word_scanner.rb +134 -0
- data/lib/rush/lexer.rb +174 -0
- data/lib/rush/loop_control_handling.rb +29 -0
- data/lib/rush/loop_nesting.rb +50 -0
- data/lib/rush/loop_runner.rb +55 -0
- data/lib/rush/option_cluster.rb +39 -0
- data/lib/rush/options.rb +76 -0
- data/lib/rush/param_text.rb +39 -0
- data/lib/rush/parser.rb +1213 -0
- data/lib/rush/parser_support.rb +112 -0
- data/lib/rush/pattern_scanner.rb +35 -0
- data/lib/rush/pending_signals.rb +57 -0
- data/lib/rush/pipeline_runner.rb +173 -0
- data/lib/rush/pipeline_statuses.rb +45 -0
- data/lib/rush/positional.rb +45 -0
- data/lib/rush/posix_pattern.rb +78 -0
- data/lib/rush/program_input.rb +89 -0
- data/lib/rush/program_reader.rb +69 -0
- data/lib/rush/program_session.rb +83 -0
- data/lib/rush/prompt.rb +52 -0
- data/lib/rush/redirect_scope.rb +64 -0
- data/lib/rush/redirection/dup_redirect.rb +58 -0
- data/lib/rush/redirection/file_redirect.rb +50 -0
- data/lib/rush/redirection/here_doc_redirect.rb +17 -0
- data/lib/rush/redirection/registry.rb +53 -0
- data/lib/rush/repl.rb +133 -0
- data/lib/rush/runtime_type_checks.rb +16 -0
- data/lib/rush/scope.rb +79 -0
- data/lib/rush/segment_buffer.rb +47 -0
- data/lib/rush/shell_parameters.rb +77 -0
- data/lib/rush/shell_pattern.rb +80 -0
- data/lib/rush/shell_state.rb +163 -0
- data/lib/rush/shell_variables.rb +67 -0
- data/lib/rush/signal_report.rb +41 -0
- data/lib/rush/signals.rb +68 -0
- data/lib/rush/source.rb +82 -0
- data/lib/rush/source_line_counter.rb +41 -0
- data/lib/rush/source_runner.rb +47 -0
- data/lib/rush/startup.rb +75 -0
- data/lib/rush/status.rb +93 -0
- data/lib/rush/stop_relay.rb +32 -0
- data/lib/rush/subshell_runner.rb +70 -0
- data/lib/rush/system_calls/collation.rb +155 -0
- data/lib/rush/system_calls/file_tests.rb +95 -0
- data/lib/rush/system_calls/process_control.rb +165 -0
- data/lib/rush/system_calls/process_identity.rb +43 -0
- data/lib/rush/system_calls/resource_limits.rb +51 -0
- data/lib/rush/system_calls.rb +206 -0
- data/lib/rush/terminal.rb +126 -0
- data/lib/rush/trap_runner.rb +169 -0
- data/lib/rush/trap_table.rb +49 -0
- data/lib/rush/umask_mode.rb +133 -0
- data/lib/rush/version.rb +6 -0
- data/lib/rush.rb +190 -0
- 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
|