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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +61 -0
  3. data/README.md +26 -14
  4. data/data/verified_gems.json +85 -63
  5. data/exe/rubycc-ar +11 -3
  6. data/include/libc/sys/cdefs.h +12 -0
  7. data/lib/rubycc/backend/aarch64.rb +705 -117
  8. data/lib/rubycc/backend/slot_residency.rb +169 -0
  9. data/lib/rubycc/backend/x86_64.rb +924 -137
  10. data/lib/rubycc/command_line.rb +339 -0
  11. data/lib/rubycc/compile_error.rb +6 -3
  12. data/lib/rubycc/compiler.rb +17 -2
  13. data/lib/rubycc/diagnostics.rb +105 -0
  14. data/lib/rubycc/doctor/gemfile.rb +12 -3
  15. data/lib/rubycc/doctor/verified_gems.rb +5 -1
  16. data/lib/rubycc/driver.rb +66 -10
  17. data/lib/rubycc/front/ast.rb +18 -7
  18. data/lib/rubycc/front/constant_evaluator.rb +12 -0
  19. data/lib/rubycc/front/lexeme_reader.rb +3 -1
  20. data/lib/rubycc/front/parser.rb +51 -18
  21. data/lib/rubycc/ir/analysis.rb +82 -0
  22. data/lib/rubycc/ir/call_convention.rb +74 -7
  23. data/lib/rubycc/ir/generator.rb +319 -9
  24. data/lib/rubycc/ir/ir.rb +39 -1
  25. data/lib/rubycc/ir/promotion.rb +255 -0
  26. data/lib/rubycc/ir/simplify.rb +570 -0
  27. data/lib/rubycc/link/library_resolver.rb +17 -5
  28. data/lib/rubycc/link/partial_linker.rb +8 -1
  29. data/lib/rubycc/link/shared_linker.rb +2 -2
  30. data/lib/rubycc/mkmf_shim.rb +178 -12
  31. data/lib/rubycc/objfile/ar_archive.rb +13 -2
  32. data/lib/rubycc/objfile/elf_reader.rb +13 -2
  33. data/lib/rubycc/pkgconf/parser.rb +4 -0
  34. data/lib/rubycc/pkgconf/resolver.rb +3 -1
  35. data/lib/rubycc/pkgconf/system_path_filter.rb +8 -2
  36. data/lib/rubycc/preprocess/preprocessor.rb +161 -37
  37. data/lib/rubycc/preprocess/scanner.rb +69 -14
  38. data/lib/rubycc/preprocess/token_converter.rb +11 -1
  39. data/lib/rubycc/rmake/cli.rb +32 -4
  40. data/lib/rubycc/rmake/executor.rb +157 -226
  41. data/lib/rubycc/rmake/makefile.rb +35 -13
  42. data/lib/rubycc/rmake/parser.rb +8 -2
  43. data/lib/rubycc/rmake/rmake.rb +1 -0
  44. data/lib/rubycc/rmake/tool_command.rb +69 -0
  45. data/lib/rubycc/shell.rb +510 -0
  46. data/lib/rubycc/type.rb +23 -7
  47. data/lib/rubycc/version.rb +1 -1
  48. data/lib/rubycc.rb +12 -0
  49. data/lib/rubygems_plugin.rb +31 -3
  50. 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
@@ -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) or `double`
219
- # (8 bytes, IEEE754 double precision). A single shared instance stands for
220
- # each (Type::Float, Type::Double); being a Data, identity and value
221
- # comparison coincide, so two `double`s name the very same type. `long
222
- # double` normalizes to `double` at parse time (same width here), so no
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubycc
4
- VERSION = "1.0.0"
4
+ VERSION = "1.1.0"
5
5
  end
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"