rubycc 1.0.0 → 1.1.0
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 +4 -4
- data/CHANGELOG.md +61 -0
- data/README.md +26 -14
- data/data/verified_gems.json +85 -63
- data/exe/rubycc-ar +11 -3
- data/include/libc/sys/cdefs.h +12 -0
- data/lib/rubycc/backend/aarch64.rb +705 -117
- data/lib/rubycc/backend/slot_residency.rb +169 -0
- data/lib/rubycc/backend/x86_64.rb +924 -137
- data/lib/rubycc/command_line.rb +339 -0
- data/lib/rubycc/compile_error.rb +6 -3
- data/lib/rubycc/compiler.rb +17 -2
- data/lib/rubycc/diagnostics.rb +105 -0
- data/lib/rubycc/doctor/gemfile.rb +12 -3
- data/lib/rubycc/doctor/verified_gems.rb +5 -1
- data/lib/rubycc/driver.rb +66 -10
- data/lib/rubycc/front/ast.rb +18 -7
- data/lib/rubycc/front/constant_evaluator.rb +12 -0
- data/lib/rubycc/front/lexeme_reader.rb +3 -1
- data/lib/rubycc/front/parser.rb +51 -18
- data/lib/rubycc/ir/analysis.rb +82 -0
- data/lib/rubycc/ir/call_convention.rb +74 -7
- data/lib/rubycc/ir/generator.rb +319 -9
- data/lib/rubycc/ir/ir.rb +39 -1
- data/lib/rubycc/ir/promotion.rb +255 -0
- data/lib/rubycc/ir/simplify.rb +570 -0
- data/lib/rubycc/link/library_resolver.rb +17 -5
- data/lib/rubycc/link/partial_linker.rb +8 -1
- data/lib/rubycc/link/shared_linker.rb +2 -2
- data/lib/rubycc/mkmf_shim.rb +178 -12
- data/lib/rubycc/objfile/ar_archive.rb +13 -2
- data/lib/rubycc/objfile/elf_reader.rb +13 -2
- data/lib/rubycc/pkgconf/parser.rb +4 -0
- data/lib/rubycc/pkgconf/resolver.rb +3 -1
- data/lib/rubycc/pkgconf/system_path_filter.rb +8 -2
- data/lib/rubycc/preprocess/preprocessor.rb +161 -37
- data/lib/rubycc/preprocess/scanner.rb +69 -14
- data/lib/rubycc/preprocess/token_converter.rb +11 -1
- data/lib/rubycc/rmake/cli.rb +32 -4
- data/lib/rubycc/rmake/executor.rb +157 -226
- data/lib/rubycc/rmake/makefile.rb +35 -13
- data/lib/rubycc/rmake/parser.rb +8 -2
- data/lib/rubycc/rmake/rmake.rb +1 -0
- data/lib/rubycc/rmake/tool_command.rb +69 -0
- data/lib/rubycc/shell.rb +510 -0
- data/lib/rubycc/type.rb +23 -7
- data/lib/rubycc/version.rb +1 -1
- data/lib/rubycc.rb +12 -0
- data/lib/rubygems_plugin.rb +31 -3
- metadata +14 -3
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../command_line"
|
|
4
|
+
|
|
5
|
+
module Rubycc
|
|
6
|
+
module Rmake
|
|
7
|
+
# Which leading words of a command name the program it runs, so rmake can
|
|
8
|
+
# tell a `$(CC)` recipe line from any other command.
|
|
9
|
+
#
|
|
10
|
+
# rmake substitutes rubycc's own Driver for the compiler and linker
|
|
11
|
+
# (Executor's tool substitution), and has to recognise those lines in a
|
|
12
|
+
# recipe. Matching a single program *name* was enough while `$(CC)` was one
|
|
13
|
+
# word, but the mkmf shim now writes `CC = <ruby> <path>/exe/rubycc`: the
|
|
14
|
+
# executables carry a `#!/usr/bin/env ruby` shebang, which a host without
|
|
15
|
+
# /usr/bin/env cannot resolve (DESIGN R5), so they are launched through the
|
|
16
|
+
# running interpreter. A rule that guessed which word was "the interpreter"
|
|
17
|
+
# would have to recognise interpreters by name, and would then mis-split
|
|
18
|
+
# `jruby <script>` or `ruby -Ilib <script>` — feeding the script path, or an
|
|
19
|
+
# interpreter flag, to the Driver as if it were a compiler argument.
|
|
20
|
+
#
|
|
21
|
+
# So nothing is guessed. The Makefile already states the command: the tool is
|
|
22
|
+
# whatever `$(CC)` (and `$(LDSHARED)`) expands to, and a recipe line runs that
|
|
23
|
+
# tool exactly when its argv *starts with* those words. Matching is prefix
|
|
24
|
+
# matching, and the words that matched are the words to drop before handing
|
|
25
|
+
# the rest to the Driver.
|
|
26
|
+
#
|
|
27
|
+
# The one adjustment is at the tail: `LDSHARED = $(CC) -shared` names the same
|
|
28
|
+
# program as `$(CC)` with a mode flag that belongs to the *Driver*, not to the
|
|
29
|
+
# launcher. Trailing option words are therefore trimmed off a prefix, so the
|
|
30
|
+
# link line keeps its `-shared` while the words that name the program are
|
|
31
|
+
# still all dropped. A prefix never ends in an option word, so trimming can
|
|
32
|
+
# never eat a flag the Driver needs.
|
|
33
|
+
module ToolCommand
|
|
34
|
+
module_function
|
|
35
|
+
|
|
36
|
+
# The words of +value+ (an expanded `$(CC)`/`$(LDSHARED)`) that name the
|
|
37
|
+
# program, or [] when it names none. Quotes are honoured — a path with a
|
|
38
|
+
# space in it is one word — and a value the splitter cannot read yields no
|
|
39
|
+
# prefix at all, which leaves those recipe lines to be spawned normally.
|
|
40
|
+
def prefix(value)
|
|
41
|
+
words = CommandLine.argv(value)
|
|
42
|
+
words.pop while !words.empty? && option?(words.last)
|
|
43
|
+
words
|
|
44
|
+
rescue CommandLine::UnsupportedSyntaxError
|
|
45
|
+
[]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Whether +argv+ runs the program named by +prefix+: every prefix word must
|
|
49
|
+
# appear, in order, at the front of argv. Each word matches literally or by
|
|
50
|
+
# basename, so a Makefile saying `gcc` still recognises a recipe's
|
|
51
|
+
# `/usr/bin/gcc` (and vice versa). An empty prefix matches nothing.
|
|
52
|
+
def match?(argv, prefix)
|
|
53
|
+
return false if prefix.empty? || prefix.length > argv.length
|
|
54
|
+
|
|
55
|
+
prefix.each_with_index.all? { |word, i| same_word?(argv[i], word) }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def same_word?(actual, expected)
|
|
59
|
+
actual == expected || File.basename(actual) == File.basename(expected)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# An option word: `-shared`, `-E`, `-Ilib`. A lone `-` is a file name by
|
|
63
|
+
# convention, not an option.
|
|
64
|
+
def option?(word)
|
|
65
|
+
word.start_with?("-") && word != "-"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
data/lib/rubycc/shell.rb
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "command_line"
|
|
4
|
+
|
|
5
|
+
module Rubycc
|
|
6
|
+
# The small piece of shell grammar rubycc interprets itself: `for`, `if`,
|
|
7
|
+
# brace groups and shell variables, layered on top of the word splitter in
|
|
8
|
+
# Rubycc::CommandLine. Automake and libtool write their install rules as one
|
|
9
|
+
# long line of exactly these constructs
|
|
10
|
+
#
|
|
11
|
+
# list=...; for p in $list; do if test -f $p; then ...; fi; done; \
|
|
12
|
+
# test -z "$list2" || { ...; }
|
|
13
|
+
#
|
|
14
|
+
# and rmake has to run them without /bin/sh, which the minimal target
|
|
15
|
+
# environment does not have (DESIGN R5).
|
|
16
|
+
#
|
|
17
|
+
# == Why a library and not an executable
|
|
18
|
+
#
|
|
19
|
+
# A `sh`-alike binary would have nothing pointing at it: `#!/bin/sh` and
|
|
20
|
+
# Ruby's own `system` carry the path to the interpreter baked in, so shipping
|
|
21
|
+
# one would not make anybody use it. And starting a process per recipe line is
|
|
22
|
+
# the cost rmake was built to avoid (M3 B3 runs the compiler in-process). As a
|
|
23
|
+
# library the interpreter is called where the recipe already is; if a command
|
|
24
|
+
# line ever needs one, a thin wrapper around #run is all it takes.
|
|
25
|
+
#
|
|
26
|
+
# == The division of labour
|
|
27
|
+
#
|
|
28
|
+
# * CommandLine is the token layer: quoting, word splitting, connectors,
|
|
29
|
+
# redirections. It is shared with the mkmf shim and knows no grammar.
|
|
30
|
+
# * Shell is the syntax layer: it recognises the compound commands, keeps the
|
|
31
|
+
# shell variables, and expands parameters.
|
|
32
|
+
# * Running a *simple* command is neither one's business. Shell hands each one
|
|
33
|
+
# to the runner the caller supplied (rmake's Executor, which owns the
|
|
34
|
+
# builtins, the tool table and the redirections). Nothing about make —
|
|
35
|
+
# targets, tools, `$(...)` — is known here.
|
|
36
|
+
#
|
|
37
|
+
# == What is deliberately absent
|
|
38
|
+
#
|
|
39
|
+
# Subshells, pipelines, command substitution, `case`, `while`, `until`,
|
|
40
|
+
# functions, background commands, here-documents, `export`, `shift` and the
|
|
41
|
+
# positional parameters all raise UnsupportedSyntaxError. The list grows only
|
|
42
|
+
# when a recipe that has to build actually needs an entry, because every
|
|
43
|
+
# construct interpreted here is one that must keep behaving identically
|
|
44
|
+
# forever — approximating a shell is the failure mode this code exists to
|
|
45
|
+
# avoid, not a goal.
|
|
46
|
+
class Shell
|
|
47
|
+
# Refusals come out as CommandLine's error: the two layers are one refusal
|
|
48
|
+
# surface to a caller (rmake wraps whichever it gets in
|
|
49
|
+
# UnsupportedRecipeError), and which layer noticed is an implementation
|
|
50
|
+
# detail — an unterminated quote is caught while splitting, a stray `fi`
|
|
51
|
+
# while parsing.
|
|
52
|
+
UnsupportedSyntaxError = CommandLine::UnsupportedSyntaxError
|
|
53
|
+
|
|
54
|
+
# A list of commands joined by connectors: [[connector, node], ...] where
|
|
55
|
+
# connector is :first, :semi, :and or :or.
|
|
56
|
+
List = Struct.new(:items)
|
|
57
|
+
# One simple command, kept as the *source text* it occupied. Expansion
|
|
58
|
+
# happens when it runs, not when it is parsed, because a variable set
|
|
59
|
+
# earlier in the same line has to be visible to it.
|
|
60
|
+
Simple = Struct.new(:text)
|
|
61
|
+
# `for NAME in WORDS; do BODY; done`. +words+ is source text too, for the
|
|
62
|
+
# same reason and because its expansion is what gets field-split.
|
|
63
|
+
For = Struct.new(:name, :words, :body)
|
|
64
|
+
# `if`: +clauses+ is [[condition, body], ...] with one entry per `if`/`elif`.
|
|
65
|
+
If = Struct.new(:clauses, :else_body)
|
|
66
|
+
# `{ BODY; }` — no subshell, so it is just its body's status.
|
|
67
|
+
Group = Struct.new(:body)
|
|
68
|
+
|
|
69
|
+
# A portable variable name.
|
|
70
|
+
NAME = /\A[A-Za-z_][A-Za-z0-9_]*\z/.freeze
|
|
71
|
+
# The default IFS: an unquoted expansion is split on runs of these.
|
|
72
|
+
IFS = /[ \t\n]+/.freeze
|
|
73
|
+
|
|
74
|
+
# The variables set so far. The caller owns the hash — rmake keeps one per
|
|
75
|
+
# recipe line, since make starts a fresh shell for every line and a variable
|
|
76
|
+
# must not leak into the next one. A name not set here is looked up in
|
|
77
|
+
# +environment+, the same table the caller will hand to the processes it
|
|
78
|
+
# spawns: `echo $PATH` and a child's own view of PATH must not disagree
|
|
79
|
+
# inside one recipe. A name in neither expands to the empty string, as in a
|
|
80
|
+
# shell without `set -u` -- which is why an unset path variable silently
|
|
81
|
+
# yields `/lib` rather than an error, exactly as it would under sh.
|
|
82
|
+
attr_reader :variables
|
|
83
|
+
|
|
84
|
+
# +runner+ (a block or a callable) receives one CommandLine::SimpleCommand —
|
|
85
|
+
# assignments, argv and redirections, already expanded and quote-stripped —
|
|
86
|
+
# and returns whether it succeeded.
|
|
87
|
+
def initialize(variables: {}, environment: {}, runner: nil, &block)
|
|
88
|
+
@variables = variables
|
|
89
|
+
@environment = environment
|
|
90
|
+
@runner = runner || block
|
|
91
|
+
raise ArgumentError, "Shell needs a runner for simple commands" unless @runner
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Interpret one line of shell text and return whether it succeeded. The
|
|
95
|
+
# whole line is parsed before anything runs, so a construct this shell does
|
|
96
|
+
# not interpret is reported instead of half-executed — which is also what a
|
|
97
|
+
# shell does with a syntax error.
|
|
98
|
+
def run(text)
|
|
99
|
+
evaluate(Parser.new(text).parse)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
def evaluate(node)
|
|
105
|
+
case node
|
|
106
|
+
when List then evaluate_list(node)
|
|
107
|
+
when Simple then evaluate_simple(node)
|
|
108
|
+
when For then evaluate_for(node)
|
|
109
|
+
when If then evaluate_if(node)
|
|
110
|
+
when Group then evaluate_list(node.body)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# An and-or list: `&&` runs the next command only after success, `||` only
|
|
115
|
+
# after failure, `;` always. One status carries left to right; an empty list
|
|
116
|
+
# is a success, as an empty shell script is.
|
|
117
|
+
def evaluate_list(list)
|
|
118
|
+
status = true
|
|
119
|
+
list.items.each do |connector, node|
|
|
120
|
+
run = case connector
|
|
121
|
+
when :first, :semi then true
|
|
122
|
+
when :and then status
|
|
123
|
+
when :or then !status
|
|
124
|
+
end
|
|
125
|
+
status = evaluate(node) if run
|
|
126
|
+
end
|
|
127
|
+
status
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# `for NAME in WORDS; do BODY; done`: the word list is expanded once, up
|
|
131
|
+
# front, and the body runs once per field with NAME set. Zero fields means
|
|
132
|
+
# zero iterations and a success — which is exactly what makes the Automake
|
|
133
|
+
# idiom (`list=; for p in $list; ...`) a no-op rather than an error.
|
|
134
|
+
def evaluate_for(node)
|
|
135
|
+
status = true
|
|
136
|
+
fields(node.words).each do |value|
|
|
137
|
+
@variables[node.name] = value
|
|
138
|
+
status = evaluate_list(node.body)
|
|
139
|
+
end
|
|
140
|
+
status
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# The first clause whose condition succeeds decides the status; with no
|
|
144
|
+
# clause taken and no `else`, an `if` succeeds (POSIX).
|
|
145
|
+
def evaluate_if(node)
|
|
146
|
+
node.clauses.each do |condition, body|
|
|
147
|
+
return evaluate_list(body) if evaluate_list(condition)
|
|
148
|
+
end
|
|
149
|
+
node.else_body ? evaluate_list(node.else_body) : true
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Expand the command's source, split it back into words, and either record
|
|
153
|
+
# an assignment or hand the command to the runner.
|
|
154
|
+
def evaluate_simple(node)
|
|
155
|
+
commands = CommandLine.parse(expand(node.text))
|
|
156
|
+
# Expanded values are respelled as quoted words (see #fields_text), so an
|
|
157
|
+
# expansion cannot introduce a connector and produce a second command.
|
|
158
|
+
unsupported!("expansion produced #{commands.length} commands", node.text) if commands.length > 1
|
|
159
|
+
return true if commands.empty?
|
|
160
|
+
|
|
161
|
+
command = commands.first[1]
|
|
162
|
+
# `VAR=value` on its own sets a shell variable that lives for the rest of
|
|
163
|
+
# the line; `VAR=value cmd` is the command's environment instead and is
|
|
164
|
+
# the runner's business, which is why only the command-less form is
|
|
165
|
+
# intercepted here. Both are what POSIX describes (XCU 2.9.1).
|
|
166
|
+
return assign(command.assignments) if command.argv.empty?
|
|
167
|
+
|
|
168
|
+
!!@runner.call(command)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def assign(assignments)
|
|
172
|
+
assignments.each do |assignment|
|
|
173
|
+
name, value = assignment.split("=", 2)
|
|
174
|
+
@variables[name] = value
|
|
175
|
+
end
|
|
176
|
+
true
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The fields an unquoted word list expands to (the `for` operand).
|
|
180
|
+
# CommandLine.tokenize rather than .parse: `for x in a=1` iterates over the
|
|
181
|
+
# word `a=1`, which .parse would have read as an assignment.
|
|
182
|
+
def fields(text)
|
|
183
|
+
CommandLine.tokenize(expand(text)).map do |token|
|
|
184
|
+
unsupported!("redirection in a `for` word list", text) unless token[0] == :word
|
|
185
|
+
token[1]
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# --- expansion --------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
# Rewrite +text+ with its parameters expanded, leaving text that
|
|
192
|
+
# CommandLine can split. Expanding into text rather than into finished
|
|
193
|
+
# words is what keeps the two layers apart: the splitting rules stay in one
|
|
194
|
+
# place. It works because an expanded value is written back *quoted* —
|
|
195
|
+
# unquoted, a value is split into fields on IFS and each field respelled as
|
|
196
|
+
# one word (CommandLine.quote), so a value holding a space becomes two
|
|
197
|
+
# words while one holding a `;` or a quote becomes data, never syntax. That
|
|
198
|
+
# is the POSIX order — expansion, then field splitting, and no further
|
|
199
|
+
# interpretation of the result (XCU 2.6).
|
|
200
|
+
def expand(text)
|
|
201
|
+
out = +""
|
|
202
|
+
i = 0
|
|
203
|
+
n = text.length
|
|
204
|
+
while i < n
|
|
205
|
+
c = text[i]
|
|
206
|
+
case c
|
|
207
|
+
when "'"
|
|
208
|
+
close = text.index("'", i + 1)
|
|
209
|
+
unsupported!("unterminated quote", text) if close.nil?
|
|
210
|
+
out << text[i..close] # single quotes suppress expansion entirely
|
|
211
|
+
i = close + 1
|
|
212
|
+
when '"'
|
|
213
|
+
segment, i = expand_double_quoted(text, i)
|
|
214
|
+
out << segment
|
|
215
|
+
when "\\"
|
|
216
|
+
# A backslash escape is carried over untouched for the splitter to
|
|
217
|
+
# remove; `\$` must not expand.
|
|
218
|
+
out << text[i, 2]
|
|
219
|
+
i += 2
|
|
220
|
+
when "`"
|
|
221
|
+
unsupported!("command substitution '`'", text)
|
|
222
|
+
when "$"
|
|
223
|
+
name, i = read_parameter(text, i)
|
|
224
|
+
out << (name ? fields_text(lookup(name)) : "$")
|
|
225
|
+
else
|
|
226
|
+
out << c
|
|
227
|
+
i += 1
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
out
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
# Expand inside a double-quoted segment, which keeps its quotes: the value
|
|
234
|
+
# is not field-split there, so it only needs the four characters that are
|
|
235
|
+
# special between double quotes escaped (CommandLine strips exactly those
|
|
236
|
+
# backslashes again).
|
|
237
|
+
def expand_double_quoted(text, i)
|
|
238
|
+
out = +'"'
|
|
239
|
+
j = i + 1
|
|
240
|
+
n = text.length
|
|
241
|
+
loop do
|
|
242
|
+
unsupported!("unterminated quote", text) if j >= n
|
|
243
|
+
|
|
244
|
+
case (c = text[j])
|
|
245
|
+
when '"'
|
|
246
|
+
out << c
|
|
247
|
+
j += 1
|
|
248
|
+
break
|
|
249
|
+
when "\\"
|
|
250
|
+
out << text[j, 2]
|
|
251
|
+
j += 2
|
|
252
|
+
when "`"
|
|
253
|
+
unsupported!("command substitution '`'", text)
|
|
254
|
+
when "$"
|
|
255
|
+
name, j = read_parameter(text, j)
|
|
256
|
+
out << (name ? quote_in_double_quotes(lookup(name)) : "$")
|
|
257
|
+
else
|
|
258
|
+
out << c
|
|
259
|
+
j += 1
|
|
260
|
+
end
|
|
261
|
+
end
|
|
262
|
+
[out, j]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Read the parameter starting at the `$` in +text+ at +i+. Returns
|
|
266
|
+
# [name, index_after] — with a nil name when the `$` names nothing and so
|
|
267
|
+
# stands for itself, which is how `$` before a space or a `/` behaves in a
|
|
268
|
+
# shell. Anything beyond a plain variable name is refused rather than
|
|
269
|
+
# approximated: `${x:-y}` and the positional and special parameters would
|
|
270
|
+
# each need semantics of their own.
|
|
271
|
+
def read_parameter(text, i)
|
|
272
|
+
nxt = text[i + 1]
|
|
273
|
+
case nxt
|
|
274
|
+
when "{"
|
|
275
|
+
close = text.index("}", i + 2)
|
|
276
|
+
unsupported!("unterminated '${'", text) if close.nil?
|
|
277
|
+
name = text[(i + 2)...close]
|
|
278
|
+
unsupported!("parameter expansion '${#{name}}'", text) unless name.match?(NAME)
|
|
279
|
+
[name, close + 1]
|
|
280
|
+
when "(" then unsupported!("command substitution '$('", text)
|
|
281
|
+
when /[A-Za-z_]/
|
|
282
|
+
j = i + 1
|
|
283
|
+
j += 1 while j < text.length && text[j].match?(/[A-Za-z0-9_]/)
|
|
284
|
+
[text[(i + 1)...j], j]
|
|
285
|
+
when /[0-9@*#?!$\-]/ then unsupported!("special parameter '$#{nxt}'", text)
|
|
286
|
+
else [nil, i + 1]
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# A variable set in this line wins; otherwise the environment answers, and a
|
|
291
|
+
# name in neither expands to the empty string (this shell has no `set -u`).
|
|
292
|
+
def lookup(name)
|
|
293
|
+
@variables.fetch(name) { @environment[name] || "" }
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# +value+ as the text of the fields it splits into, each respelled so the
|
|
297
|
+
# splitter reads it back as one word. An empty (or all-blank) value yields
|
|
298
|
+
# no words at all, which is why `for p in $empty` iterates zero times and
|
|
299
|
+
# `cmd $empty` is `cmd`.
|
|
300
|
+
#
|
|
301
|
+
# The one place this rewrite is not the shell's answer: a value that looks
|
|
302
|
+
# like `a=1` expanding into the command-name position is read back as an
|
|
303
|
+
# assignment, where a shell would have decided that before expanding. No
|
|
304
|
+
# recipe writes that, and telling the two apart would mean moving the
|
|
305
|
+
# assignment rule out of the splitter, where both callers need it.
|
|
306
|
+
def fields_text(value)
|
|
307
|
+
value.split(IFS).reject(&:empty?).map { |field| CommandLine.quote(field) }.join(" ")
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def quote_in_double_quotes(value)
|
|
311
|
+
value.gsub(/[\\"$`]/) { |c| "\\#{c}" }
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def unsupported!(construct, text)
|
|
315
|
+
raise UnsupportedSyntaxError.new(construct, text)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Builds the syntax tree of one line. It works on CommandLine's tokens plus
|
|
319
|
+
# their spans: the structure is decided by the *unexpanded* words (a shell
|
|
320
|
+
# recognises its grammar before it expands anything), and each simple
|
|
321
|
+
# command is remembered as the slice of the line it came from so that its
|
|
322
|
+
# expansion can be deferred to the moment it runs.
|
|
323
|
+
class Parser
|
|
324
|
+
# The words this grammar gives a meaning to. A word outside the list is
|
|
325
|
+
# an ordinary command name; one inside it that turns up where the grammar
|
|
326
|
+
# does not expect it (a stray `fi`) is a syntax error, not a command.
|
|
327
|
+
KEYWORDS = CommandLine::COMPOUND_WORDS
|
|
328
|
+
|
|
329
|
+
def initialize(text)
|
|
330
|
+
@text = text
|
|
331
|
+
@tokens = CommandLine.tokenize_spans(text)
|
|
332
|
+
@i = 0
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def parse
|
|
336
|
+
list = parse_list([])
|
|
337
|
+
unsupported!("unexpected #{describe(peek)}") unless at_end?
|
|
338
|
+
list
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
private
|
|
342
|
+
|
|
343
|
+
def parse_list(stops)
|
|
344
|
+
items = []
|
|
345
|
+
connector = :first
|
|
346
|
+
loop do
|
|
347
|
+
break if at_end? || stops.include?(bare_word)
|
|
348
|
+
|
|
349
|
+
items << [connector, parse_command]
|
|
350
|
+
break unless (next_connector = connector_type)
|
|
351
|
+
|
|
352
|
+
connector = next_connector
|
|
353
|
+
@i += 1
|
|
354
|
+
end
|
|
355
|
+
List.new(items)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def parse_command
|
|
359
|
+
case (word = bare_word)
|
|
360
|
+
when "for" then parse_for
|
|
361
|
+
when "if" then parse_if
|
|
362
|
+
when "{" then parse_group
|
|
363
|
+
else
|
|
364
|
+
# Every keyword the grammar expects is consumed by the clause that
|
|
365
|
+
# expects it, so one reaching here is misplaced.
|
|
366
|
+
unsupported!("unexpected '#{word}'") if KEYWORDS.include?(word)
|
|
367
|
+
parse_simple
|
|
368
|
+
end
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Everything up to the next connector. Only the first word of a command
|
|
372
|
+
# can be a keyword (XCU 2.9), so `echo done` needs no special care: the
|
|
373
|
+
# scan stops at connectors and nothing else.
|
|
374
|
+
def parse_simple
|
|
375
|
+
# A reserved word no layer interprets (`while`, `case`, `!`) is refused
|
|
376
|
+
# while parsing, not when the command would run, so that the line is
|
|
377
|
+
# rejected as a whole: `echo a; while ...` must not echo first.
|
|
378
|
+
# CommandLine refuses it again when the command is split, for a caller
|
|
379
|
+
# that reaches it without coming through here.
|
|
380
|
+
if (word = bare_word) && CommandLine::RESERVED_WORDS.include?(word)
|
|
381
|
+
unsupported!("shell reserved word '#{word}'")
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
start = @i
|
|
385
|
+
@i += 1 while !at_end? && connector_type.nil?
|
|
386
|
+
# Nothing at all (`;;`, a leading `;`) is an empty command, which the
|
|
387
|
+
# splitter has always let through as a no-op success.
|
|
388
|
+
Simple.new(start == @i ? "" : source(start, @i - 1))
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def parse_for
|
|
392
|
+
@i += 1
|
|
393
|
+
name = bare_word
|
|
394
|
+
unsupported!("`for` without a variable name") unless name&.match?(NAME)
|
|
395
|
+
|
|
396
|
+
@i += 1
|
|
397
|
+
# `for x; do ...` iterates over the positional parameters, which this
|
|
398
|
+
# shell does not have; only the explicit word list is interpreted.
|
|
399
|
+
unsupported!("`for` without `in`") unless bare_word == "in"
|
|
400
|
+
|
|
401
|
+
@i += 1
|
|
402
|
+
words = parse_for_words
|
|
403
|
+
# The word list is closed by the `;` (POSIX's sequential_sep), not by
|
|
404
|
+
# the `do` itself: `for p in a do b; do ...` loops over three words,
|
|
405
|
+
# `do` among them. Verified against /bin/sh, which likewise refuses
|
|
406
|
+
# `for p in a b do ...; done` for the missing separator.
|
|
407
|
+
unsupported!("`for` word list without a `;` before `do`") unless connector_type == :semi
|
|
408
|
+
|
|
409
|
+
@i += 1
|
|
410
|
+
expect!("do")
|
|
411
|
+
body = parse_list(%w[done])
|
|
412
|
+
expect!("done")
|
|
413
|
+
For.new(name, words, body)
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
# The operand list of a `for`, kept as source text.
|
|
417
|
+
def parse_for_words
|
|
418
|
+
start = @i
|
|
419
|
+
@i += 1 while word?
|
|
420
|
+
start == @i ? "" : source(start, @i - 1)
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def parse_if
|
|
424
|
+
@i += 1
|
|
425
|
+
clauses = []
|
|
426
|
+
loop do
|
|
427
|
+
condition = parse_list(%w[then])
|
|
428
|
+
expect!("then")
|
|
429
|
+
clauses << [condition, parse_list(%w[elif else fi])]
|
|
430
|
+
break unless bare_word == "elif"
|
|
431
|
+
|
|
432
|
+
@i += 1
|
|
433
|
+
end
|
|
434
|
+
else_body = nil
|
|
435
|
+
if bare_word == "else"
|
|
436
|
+
@i += 1
|
|
437
|
+
else_body = parse_list(%w[fi])
|
|
438
|
+
end
|
|
439
|
+
expect!("fi")
|
|
440
|
+
If.new(clauses, else_body)
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def parse_group
|
|
444
|
+
@i += 1
|
|
445
|
+
body = parse_list(%w[}])
|
|
446
|
+
expect!("}")
|
|
447
|
+
Group.new(body)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# --- token access ---------------------------------------------------
|
|
451
|
+
|
|
452
|
+
def peek
|
|
453
|
+
@tokens[@i]
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def at_end?
|
|
457
|
+
@i >= @tokens.length
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
def word?
|
|
461
|
+
!at_end? && peek[0][0] == :word
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
def connector_type
|
|
465
|
+
return nil if at_end?
|
|
466
|
+
|
|
467
|
+
type = peek[0][0]
|
|
468
|
+
%i[semi and or].include?(type) ? type : nil
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
# The word at the cursor when it is spelled bare, else nil — the form the
|
|
472
|
+
# grammar reads, since quoting takes a word's keyword-hood away: `"for"`
|
|
473
|
+
# is a command named `for`, not a loop (XCU 2.9). The span is what tells
|
|
474
|
+
# the two apart, quote removal having already made them equal.
|
|
475
|
+
def bare_word
|
|
476
|
+
return nil unless word?
|
|
477
|
+
|
|
478
|
+
raw = @text[peek[1]]
|
|
479
|
+
raw == peek[0][1] ? raw : nil
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
def expect!(word)
|
|
483
|
+
unsupported!("expected `#{word}`, found #{describe(peek)}") unless bare_word == word
|
|
484
|
+
|
|
485
|
+
@i += 1
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# The source text spanning tokens +first+ through +last+, inclusive.
|
|
489
|
+
def source(first, last)
|
|
490
|
+
@text[@tokens[first][1].begin...@tokens[last][1].end]
|
|
491
|
+
end
|
|
492
|
+
|
|
493
|
+
def describe(token)
|
|
494
|
+
return "end of line" if token.nil?
|
|
495
|
+
|
|
496
|
+
case token[0][0]
|
|
497
|
+
when :word then "'#{token[0][1]}'"
|
|
498
|
+
when :semi then "';'"
|
|
499
|
+
when :and then "'&&'"
|
|
500
|
+
when :or then "'||'"
|
|
501
|
+
else "a redirection"
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def unsupported!(construct)
|
|
506
|
+
raise UnsupportedSyntaxError.new(construct, @text)
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
end
|
|
510
|
+
end
|
data/lib/rubycc/type.rb
CHANGED
|
@@ -215,17 +215,29 @@ module Rubycc
|
|
|
215
215
|
end
|
|
216
216
|
end
|
|
217
217
|
|
|
218
|
-
# A floating type: `float` (4 bytes, IEEE754 single precision)
|
|
219
|
-
# (8 bytes, IEEE754 double precision)
|
|
220
|
-
# each (Type::Float, Type::Double);
|
|
221
|
-
# comparison coincide, so two `double`s name
|
|
222
|
-
#
|
|
223
|
-
# separate instance exists. #arithmetic? is true — a floating type mixes with
|
|
218
|
+
# A floating type: `float` (4 bytes, IEEE754 single precision), `double`
|
|
219
|
+
# (8 bytes, IEEE754 double precision) or `long double`. A single shared
|
|
220
|
+
# instance stands for each (Type::Float, Type::Double, Type::LongDouble);
|
|
221
|
+
# being a Data, identity and value comparison coincide, so two `double`s name
|
|
222
|
+
# the very same type. #arithmetic? is true — a floating type mixes with
|
|
224
223
|
# the integer types under the usual arithmetic conversions — while #integer?
|
|
225
224
|
# is false and #float? true, which is how the generator tells a floating
|
|
226
225
|
# operand apart to emit the f-prefixed IR (:fadd, :flt, ...) and the
|
|
227
226
|
# integer/float conversions (:itof / :ftoi / :ftof).
|
|
228
227
|
#
|
|
228
|
+
# `long double` is a distinct *name* over the same 8-byte representation a
|
|
229
|
+
# `double` has: every value it holds is a double, every operation on it is a
|
|
230
|
+
# double's, and #size stays 8 (widening it would move sizeof, struct layouts
|
|
231
|
+
# and max_align_t, which belongs to a whole-ABI change and not here). The
|
|
232
|
+
# separate instance exists for one reason — a variadic call has to know a
|
|
233
|
+
# `long double` argument was written as one, because the callee reads it in
|
|
234
|
+
# the target's own long-double format (x87 80-bit extended on x86-64, IEEE
|
|
235
|
+
# binary128 on aarch64) and at the target's own ABI position, both of which
|
|
236
|
+
# differ from a `double`'s. The generator converts the value at that one
|
|
237
|
+
# boundary (see IR::Generator#lower_variadic_long_double); everywhere else
|
|
238
|
+
# the two types behave alike, which is why the usual arithmetic conversions
|
|
239
|
+
# settle on `double` (Type::LongDouble's own width) rather than on it.
|
|
240
|
+
#
|
|
229
241
|
# Value representation (see Backend::X86_64): a `float` value lives in its
|
|
230
242
|
# virtual-register slot's low 4 bytes as an IEEE754 single-precision bit
|
|
231
243
|
# pattern, a `double` in the whole 8-byte slot as a double-precision one; a
|
|
@@ -328,9 +340,13 @@ module Rubycc
|
|
|
328
340
|
Int128 = IntegerType.new("__int128", 16, true)
|
|
329
341
|
UInt128 = IntegerType.new("unsigned __int128", 16, false)
|
|
330
342
|
|
|
331
|
-
# The shared floating-type instances (Type::Float, Type::Double
|
|
343
|
+
# The shared floating-type instances (Type::Float, Type::Double,
|
|
344
|
+
# Type::LongDouble). `long double` deliberately reports the same 8-byte
|
|
345
|
+
# width `double` does — see FloatType's comment for what that models and
|
|
346
|
+
# what it does not.
|
|
332
347
|
Float = FloatType.new("float", 4)
|
|
333
348
|
Double = FloatType.new("double", 8)
|
|
349
|
+
LongDouble = FloatType.new("long double", 8)
|
|
334
350
|
|
|
335
351
|
# The lone `void`. Referred to everywhere as Type::Void.
|
|
336
352
|
Void = VoidType.new
|
data/lib/rubycc/version.rb
CHANGED
data/lib/rubycc.rb
CHANGED
|
@@ -2,10 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "rubycc/version"
|
|
4
4
|
|
|
5
|
+
# Every file rubycc reads — C source, a .pc file, a Makefile, a Gemfile, a
|
|
6
|
+
# linker script — is read as bytes (File.binread), and every path it builds is a
|
|
7
|
+
# byte string. The reason is a property of Ruby rather than of any one reader:
|
|
8
|
+
# two strings holding the same non-ASCII bytes under different encodings are
|
|
9
|
+
# neither == nor eql?, hash differently, and cannot be joined or concatenated at
|
|
10
|
+
# all — while File.read tags what it returns with Encoding.default_external,
|
|
11
|
+
# which is the locale, which is US-ASCII when there is none. Strings that enter
|
|
12
|
+
# from the process instead (ARGV, ENV, Dir.pwd, a directory listing) are
|
|
13
|
+
# therefore re-tagged with String#b at the class boundary they cross;
|
|
14
|
+
# `text.b unless text.encoding == Encoding::BINARY` is the spelling for that,
|
|
15
|
+
# leaving a string that is already bytes alone rather than duplicating it.
|
|
5
16
|
module Rubycc
|
|
6
17
|
class Error < StandardError; end
|
|
7
18
|
end
|
|
8
19
|
|
|
20
|
+
require_relative "rubycc/diagnostics"
|
|
9
21
|
require_relative "rubycc/compile_error"
|
|
10
22
|
require_relative "rubycc/type"
|
|
11
23
|
require_relative "rubycc/front/token"
|