rubycc 1.0.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 (159) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +56 -0
  3. data/LICENSE.txt +21 -0
  4. data/NOTICE +52 -0
  5. data/README.md +208 -0
  6. data/data/README.md +117 -0
  7. data/data/r10_corpus_scan.json +4082 -0
  8. data/data/r10_manual_classification.json +5342 -0
  9. data/data/r10_verification_m4a.json +1531 -0
  10. data/data/r10_verification_m4b.json +1818 -0
  11. data/data/r10_verification_m4c.json +1489 -0
  12. data/data/r10_verification_m4d.json +318 -0
  13. data/data/r10_verification_rbs.json +192 -0
  14. data/data/verified_gems.json +397 -0
  15. data/exe/rmake +16 -0
  16. data/exe/rubycc +9 -0
  17. data/exe/rubycc-ar +114 -0
  18. data/exe/rubycc-doctor +14 -0
  19. data/exe/rubycc-pkgconf +8 -0
  20. data/include/float.h +87 -0
  21. data/include/iso646.h +18 -0
  22. data/include/libc/alloca.h +18 -0
  23. data/include/libc/arpa/inet.h +61 -0
  24. data/include/libc/assert.h +43 -0
  25. data/include/libc/dirent.h +72 -0
  26. data/include/libc/dlfcn.h +58 -0
  27. data/include/libc/features.h +205 -0
  28. data/include/libc/glibc/aarch64/ctype.h +108 -0
  29. data/include/libc/glibc/aarch64/endian.h +54 -0
  30. data/include/libc/glibc/aarch64/errno.h +154 -0
  31. data/include/libc/glibc/aarch64/fcntl.h +144 -0
  32. data/include/libc/glibc/aarch64/inttypes.h +182 -0
  33. data/include/libc/glibc/aarch64/limits.h +63 -0
  34. data/include/libc/glibc/aarch64/pthread.h +131 -0
  35. data/include/libc/glibc/aarch64/setjmp.h +84 -0
  36. data/include/libc/glibc/aarch64/stdint.h +174 -0
  37. data/include/libc/glibc/aarch64/sys/epoll.h +88 -0
  38. data/include/libc/glibc/aarch64/sys/fcntl.h +19 -0
  39. data/include/libc/glibc/aarch64/sys/select.h +75 -0
  40. data/include/libc/glibc/aarch64/sys/stat.h +136 -0
  41. data/include/libc/glibc/aarch64/sys/syscall.h +167 -0
  42. data/include/libc/glibc/aarch64/sys/time.h +74 -0
  43. data/include/libc/glibc/aarch64/sys/types.h +127 -0
  44. data/include/libc/glibc/aarch64/time.h +113 -0
  45. data/include/libc/glibc/x86_64/ctype.h +108 -0
  46. data/include/libc/glibc/x86_64/endian.h +54 -0
  47. data/include/libc/glibc/x86_64/errno.h +154 -0
  48. data/include/libc/glibc/x86_64/fcntl.h +134 -0
  49. data/include/libc/glibc/x86_64/inttypes.h +182 -0
  50. data/include/libc/glibc/x86_64/limits.h +61 -0
  51. data/include/libc/glibc/x86_64/pthread.h +111 -0
  52. data/include/libc/glibc/x86_64/setjmp.h +82 -0
  53. data/include/libc/glibc/x86_64/stdint.h +163 -0
  54. data/include/libc/glibc/x86_64/sys/epoll.h +91 -0
  55. data/include/libc/glibc/x86_64/sys/fcntl.h +19 -0
  56. data/include/libc/glibc/x86_64/sys/select.h +75 -0
  57. data/include/libc/glibc/x86_64/sys/stat.h +130 -0
  58. data/include/libc/glibc/x86_64/sys/syscall.h +188 -0
  59. data/include/libc/glibc/x86_64/sys/time.h +74 -0
  60. data/include/libc/glibc/x86_64/sys/types.h +123 -0
  61. data/include/libc/glibc/x86_64/time.h +113 -0
  62. data/include/libc/grp.h +50 -0
  63. data/include/libc/langinfo.h +126 -0
  64. data/include/libc/link.h +18 -0
  65. data/include/libc/locale.h +83 -0
  66. data/include/libc/math.h +191 -0
  67. data/include/libc/netinet/in.h +126 -0
  68. data/include/libc/netinet/tcp.h +44 -0
  69. data/include/libc/poll.h +46 -0
  70. data/include/libc/pwd.h +58 -0
  71. data/include/libc/regex.h +53 -0
  72. data/include/libc/sched.h +35 -0
  73. data/include/libc/signal.h +204 -0
  74. data/include/libc/stdio.h +157 -0
  75. data/include/libc/stdlib.h +92 -0
  76. data/include/libc/string.h +74 -0
  77. data/include/libc/strings.h +25 -0
  78. data/include/libc/sys/cdefs.h +126 -0
  79. data/include/libc/sys/inotify.h +111 -0
  80. data/include/libc/sys/ioctl.h +36 -0
  81. data/include/libc/sys/mman.h +65 -0
  82. data/include/libc/sys/param.h +41 -0
  83. data/include/libc/sys/resource.h +109 -0
  84. data/include/libc/sys/socket.h +167 -0
  85. data/include/libc/sys/statfs.h +82 -0
  86. data/include/libc/sys/timerfd.h +55 -0
  87. data/include/libc/sys/uio.h +40 -0
  88. data/include/libc/sys/un.h +25 -0
  89. data/include/libc/sys/utsname.h +35 -0
  90. data/include/libc/sys/wait.h +135 -0
  91. data/include/libc/termios.h +179 -0
  92. data/include/libc/unistd.h +194 -0
  93. data/include/stdalign.h +16 -0
  94. data/include/stdarg.h +31 -0
  95. data/include/stdatomic.h +158 -0
  96. data/include/stdbool.h +15 -0
  97. data/include/stdckdint.h +28 -0
  98. data/include/stddef.h +60 -0
  99. data/include/stdnoreturn.h +18 -0
  100. data/include/x86intrin.h +16 -0
  101. data/lib/rubycc/backend/aarch64.rb +1724 -0
  102. data/lib/rubycc/backend/x86_64.rb +1369 -0
  103. data/lib/rubycc/compile_error.rb +36 -0
  104. data/lib/rubycc/compiler.rb +305 -0
  105. data/lib/rubycc/doctor/builder.rb +151 -0
  106. data/lib/rubycc/doctor/cli.rb +190 -0
  107. data/lib/rubycc/doctor/fetcher.rb +84 -0
  108. data/lib/rubycc/doctor/gemfile.rb +115 -0
  109. data/lib/rubycc/doctor/verified_gems.rb +114 -0
  110. data/lib/rubycc/doctor.rb +16 -0
  111. data/lib/rubycc/driver.rb +463 -0
  112. data/lib/rubycc/front/ast.rb +528 -0
  113. data/lib/rubycc/front/constant_evaluator.rb +631 -0
  114. data/lib/rubycc/front/initializer_resolver.rb +592 -0
  115. data/lib/rubycc/front/lexeme_reader.rb +460 -0
  116. data/lib/rubycc/front/lexer.rb +232 -0
  117. data/lib/rubycc/front/parser.rb +4122 -0
  118. data/lib/rubycc/front/token.rb +49 -0
  119. data/lib/rubycc/ir/call_convention.rb +486 -0
  120. data/lib/rubycc/ir/generator.rb +6036 -0
  121. data/lib/rubycc/ir/ir.rb +417 -0
  122. data/lib/rubycc/link/compat_runtime.rb +89 -0
  123. data/lib/rubycc/link/errors.rb +13 -0
  124. data/lib/rubycc/link/executable_linker.rb +405 -0
  125. data/lib/rubycc/link/library_resolver.rb +437 -0
  126. data/lib/rubycc/link/partial_linker.rb +546 -0
  127. data/lib/rubycc/link/shared_linker.rb +1732 -0
  128. data/lib/rubycc/mkmf_shim.rb +83 -0
  129. data/lib/rubycc/objfile/ar_archive.rb +437 -0
  130. data/lib/rubycc/objfile/elf_reader.rb +646 -0
  131. data/lib/rubycc/objfile/elf_writer.rb +891 -0
  132. data/lib/rubycc/objfile/relocatable_writer.rb +376 -0
  133. data/lib/rubycc/pkgconf/cli.rb +139 -0
  134. data/lib/rubycc/pkgconf/errors.rb +39 -0
  135. data/lib/rubycc/pkgconf/model.rb +28 -0
  136. data/lib/rubycc/pkgconf/parser.rb +109 -0
  137. data/lib/rubycc/pkgconf/pkgconf.rb +15 -0
  138. data/lib/rubycc/pkgconf/resolver.rb +70 -0
  139. data/lib/rubycc/pkgconf/search_path.rb +38 -0
  140. data/lib/rubycc/pkgconf/system_path_filter.rb +131 -0
  141. data/lib/rubycc/preprocess/constant_expression.rb +219 -0
  142. data/lib/rubycc/preprocess/glibc_version.rb +124 -0
  143. data/lib/rubycc/preprocess/pp_token.rb +62 -0
  144. data/lib/rubycc/preprocess/preprocessor.rb +2020 -0
  145. data/lib/rubycc/preprocess/scanner.rb +290 -0
  146. data/lib/rubycc/preprocess/token_converter.rb +157 -0
  147. data/lib/rubycc/rmake/cli.rb +143 -0
  148. data/lib/rubycc/rmake/errors.rb +71 -0
  149. data/lib/rubycc/rmake/executor.rb +818 -0
  150. data/lib/rubycc/rmake/expander.rb +251 -0
  151. data/lib/rubycc/rmake/makefile.rb +352 -0
  152. data/lib/rubycc/rmake/model.rb +137 -0
  153. data/lib/rubycc/rmake/parser.rb +226 -0
  154. data/lib/rubycc/rmake/rmake.rb +15 -0
  155. data/lib/rubycc/type.rb +1236 -0
  156. data/lib/rubycc/version.rb +5 -0
  157. data/lib/rubycc.rb +36 -0
  158. data/lib/rubygems_plugin.rb +102 -0
  159. metadata +219 -0
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Pkgconf — the pkg-config shim (M3 / DESIGN R5, ROADMAP §6 B4): a pure-Ruby
4
+ # .pc parser plus the narrow CLI mkmf's mkmf.rb#pkg_config actually invokes
5
+ # ($PKGCONFIG with --exists / --modversion / --cflags[-only-*] /
6
+ # --libs[-only-l]). It stays self-contained — depending on nothing else in
7
+ # lib/rubycc, like rmake (M3 B1) — so exe/rubycc-pkgconf and the tests can
8
+ # load it on its own.
9
+ require_relative "errors"
10
+ require_relative "model"
11
+ require_relative "parser"
12
+ require_relative "search_path"
13
+ require_relative "system_path_filter"
14
+ require_relative "resolver"
15
+ require_relative "cli"
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "shellwords"
4
+
5
+ module Rubycc
6
+ module Pkgconf
7
+ # Loads .pc files by name from a search path and walks their Requires
8
+ # chain to produce the token lists --cflags/--libs need. A module is
9
+ # loaded at most once per Resolver (a memoizing cache doubling as the
10
+ # cycle guard for a Requires loop); within a single module's own
11
+ # Cflags/Libs value, tokens are kept exactly as written — no de-duplication
12
+ # is attempted (see the Step 59 report: this cannot be measured against
13
+ # the real pkg-config in this environment, so the simplest order-preserving
14
+ # concatenation is used, which is all mkmf's pkg_config() needs).
15
+ class Resolver
16
+ def initialize(directories: SearchPath.directories)
17
+ @directories = directories
18
+ @cache = {}
19
+ end
20
+
21
+ # True if +name+ and its whole Requires/Requires.private chain resolve —
22
+ # the same thing `pkg-config --exists` has to check, since it must load
23
+ # every dependency's .pc file to answer the question.
24
+ def exists?(name)
25
+ cflags_tokens(name)
26
+ true
27
+ rescue PkgconfError
28
+ false
29
+ end
30
+
31
+ def load(name)
32
+ return @cache[name] if @cache.key?(name)
33
+
34
+ path = SearchPath.find(name, directories: @directories)
35
+ raise NotFoundError, "package '#{name}' not found (searched #{@directories.join(File::PATH_SEPARATOR)})" unless path
36
+
37
+ @cache[name] = Parser.parse(File.read(path), path: path)
38
+ end
39
+
40
+ # Own Cflags, then each public Requires recursively, then each
41
+ # Requires.private recursively — a private dependency's compiler flags
42
+ # are still needed to build against +name+, they are just not exposed to
43
+ # *its* dependents (that asymmetry is Requires.private's whole point).
44
+ def cflags_tokens(name, visited = {})
45
+ return [] if visited[name]
46
+
47
+ visited[name] = true
48
+ pkg = load(name)
49
+ tokens = Shellwords.split(pkg.cflags)
50
+ pkg.requires.each { |dep| tokens.concat(cflags_tokens(dep, visited)) }
51
+ pkg.requires_private.each { |dep| tokens.concat(cflags_tokens(dep, visited)) }
52
+ tokens
53
+ end
54
+
55
+ # Own Libs, then each public Requires recursively. Requires.private
56
+ # never contributes here — pkg-config only pulls a private dependency's
57
+ # Libs into the static-link case (Libs.private via --static), which
58
+ # mkmf's pkg_config() never requests, so it is out of scope.
59
+ def libs_tokens(name, visited = {})
60
+ return [] if visited[name]
61
+
62
+ visited[name] = true
63
+ pkg = load(name)
64
+ tokens = Shellwords.split(pkg.libs)
65
+ pkg.requires.each { |dep| tokens.concat(libs_tokens(dep, visited)) }
66
+ tokens
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubycc
4
+ module Pkgconf
5
+ # Where a `<name>.pc` is looked up when nothing else says otherwise.
6
+ module SearchPath
7
+ # The multiarch directory comes first because that is where this
8
+ # environment's real .pc files actually live
9
+ # (/usr/lib/x86_64-linux-gnu/pkgconfig/{zlib,libffi,openssl,...}.pc —
10
+ # see the Step 59 report); /usr/lib/pkgconfig and /usr/share/pkgconfig
11
+ # are the architecture-independent locations pkg-config falls back to
12
+ # next on a Debian/Ubuntu multiarch system.
13
+ DEFAULT_DIRECTORIES = [
14
+ "/usr/lib/x86_64-linux-gnu/pkgconfig",
15
+ "/usr/lib/pkgconfig",
16
+ "/usr/share/pkgconfig"
17
+ ].freeze
18
+
19
+ # PKG_CONFIG_PATH (colon-separated, mirroring PATH) is searched before
20
+ # the defaults — the same precedence mkmf's own pkg_config() relies on
21
+ # when dir_config found a --with-*-dir libdir and set PKG_CONFIG_PATH to
22
+ # that libdir's pkgconfig/ subdirectory before invoking pkg-config.
23
+ def self.directories(env = ENV)
24
+ extra = (env["PKG_CONFIG_PATH"] || "").split(File::PATH_SEPARATOR).reject(&:empty?)
25
+ extra + DEFAULT_DIRECTORIES
26
+ end
27
+
28
+ # The first `<name>.pc` found across +directories+, or nil.
29
+ def self.find(name, directories: directories())
30
+ directories.each do |dir|
31
+ path = File.join(dir, "#{name}.pc")
32
+ return path if File.file?(path)
33
+ end
34
+ nil
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubycc
4
+ module Pkgconf
5
+ # Drops the `-I<dir>`/`-L<dir>` tokens whose directory the toolchain
6
+ # already searches on its own. pkg-config suppresses those in its
7
+ # --cflags/--libs output (emitting them would let a package silently
8
+ # reorder the compiler's or linker's default search order), so a shim that
9
+ # kept them would disagree with the real tool on the most common packages
10
+ # of all: this environment's zlib.pc alone carries `-I/usr/include` and
11
+ # `-L${libdir}`.
12
+ #
13
+ # The two escape hatches are the ones the environment exposes:
14
+ # PKG_CONFIG_ALLOW_SYSTEM_CFLAGS / PKG_CONFIG_ALLOW_SYSTEM_LIBS turn the
15
+ # respective filter off, and PKG_CONFIG_SYSTEM_INCLUDE_PATH /
16
+ # PKG_CONFIG_SYSTEM_LIBRARY_PATH (colon-separated, mirroring PATH) replace
17
+ # the directory lists below.
18
+ class SystemPathFilter
19
+ # What counts as a system directory when nothing overrides it.
20
+ DEFAULT_INCLUDE_DIRECTORIES = ["/usr/include"].freeze
21
+
22
+ # What counts as a system library directory, in two groups.
23
+ #
24
+ # The multiarch pair is here because CI measured the real pkg-config
25
+ # against a Debian zlib.pc (libdir=${prefix}/lib/x86_64-linux-gnu) and
26
+ # found it drops the `-L` for that directory too, not just plain /usr/lib:
27
+ # on Debian/Ubuntu the multiarch libdir *is* a system libdir as far as
28
+ # pkg-config is concerned. It is applied only when the directory exists,
29
+ # because a multiarch libdir is a Debian/Ubuntu layout: on a host without
30
+ # one (Alpine, measured in CI) the real pkg-config keeps a `-L` naming it,
31
+ # since there it is an ordinary directory rather than one the linker
32
+ # already searches. Dropping it there made this shim emit `-lz` where the
33
+ # real tool emitted `-L/usr/lib/x86_64-linux-gnu -lz` (docs/development/STEPS.md Step
34
+ # 196).
35
+ #
36
+ # /usr/lib, /lib and /usr/lib64 are unconditional: they are system libdirs
37
+ # wherever they exist, and gating them on existence would change what this
38
+ # shim emits on the hosts the other measurements were taken on.
39
+ #
40
+ # The lists are deliberately kept in step with the other hardcoded
41
+ # "default search directory" lists in this codebase --
42
+ # Link::LibraryResolver::DEFAULT_SYSTEM_DIRS and
43
+ # SearchPath::DEFAULT_DIRECTORIES both already special-case the same
44
+ # multiarch path -- on the theory that a directory this shim's own linker
45
+ # searches by default is never worth emitting a `-L` for; that is the
46
+ # whole point of this filter.
47
+ #
48
+ # /usr/local/lib is deliberately *not* included even though
49
+ # LibraryResolver::DEFAULT_SYSTEM_DIRS lists it: the real pkg-config does
50
+ # not treat it as a system directory, and adding it would make this shim
51
+ # diverge from the measured behaviour instead of matching it.
52
+ MULTIARCH_LIBRARY_DIRECTORIES = [
53
+ "/usr/lib/x86_64-linux-gnu",
54
+ "/lib/x86_64-linux-gnu"
55
+ ].freeze
56
+
57
+ UNIVERSAL_LIBRARY_DIRECTORIES = [
58
+ "/usr/lib",
59
+ "/lib",
60
+ "/usr/lib64"
61
+ ].freeze
62
+
63
+ # The system libdirs on *this* host, in the order above.
64
+ def self.default_library_directories
65
+ MULTIARCH_LIBRARY_DIRECTORIES.select { |d| File.directory?(d) } + UNIVERSAL_LIBRARY_DIRECTORIES
66
+ end
67
+
68
+ # +env+ is an argument (defaulting to the process environment) for the
69
+ # same reason SearchPath.directories takes one: it keeps the lookup
70
+ # injectable from a test without mutating the global ENV.
71
+ def initialize(env = ENV)
72
+ @include_directories = directory_list(env["PKG_CONFIG_SYSTEM_INCLUDE_PATH"], DEFAULT_INCLUDE_DIRECTORIES)
73
+ @library_directories = directory_list(env["PKG_CONFIG_SYSTEM_LIBRARY_PATH"],
74
+ self.class.default_library_directories)
75
+ @keep_system_includes = set?(env["PKG_CONFIG_ALLOW_SYSTEM_CFLAGS"])
76
+ @keep_system_libraries = set?(env["PKG_CONFIG_ALLOW_SYSTEM_LIBS"])
77
+ end
78
+
79
+ # +tokens+ with the system `-I<dir>` entries removed. Every other token
80
+ # (`-D...`, a bare `-I`, `-l...`, ...) is passed through untouched.
81
+ def cflags(tokens)
82
+ return tokens if @keep_system_includes
83
+
84
+ reject_system(tokens, "-I", @include_directories)
85
+ end
86
+
87
+ # +tokens+ with the system `-L<dir>` entries removed; `-l` and the rest
88
+ # are passed through untouched.
89
+ def libs(tokens)
90
+ return tokens if @keep_system_libraries
91
+
92
+ reject_system(tokens, "-L", @library_directories)
93
+ end
94
+
95
+ private
96
+
97
+ # Only the one-token `-I/usr/include` spelling is recognized. A `.pc`
98
+ # that writes the directory as a separate token (`-I /usr/include`)
99
+ # already defeats this shim's --cflags-only-I/--cflags-only-other split,
100
+ # which classifies per token, so nothing here tries to pair the two up.
101
+ def reject_system(tokens, flag, system_directories)
102
+ tokens.reject do |token|
103
+ token.start_with?(flag) && token.length > flag.length &&
104
+ system_directories.include?(normalize(token[flag.length..]))
105
+ end
106
+ end
107
+
108
+ # An explicit value replaces the default outright, even when it is empty
109
+ # (`PKG_CONFIG_SYSTEM_INCLUDE_PATH=` then means "no directory is a
110
+ # system directory"); only an unset variable falls back to +defaults+.
111
+ def directory_list(value, defaults)
112
+ list = value.nil? ? defaults : value.split(File::PATH_SEPARATOR).reject(&:empty?)
113
+ list.map { |dir| normalize(dir) }
114
+ end
115
+
116
+ def set?(value)
117
+ !value.nil? && !value.empty?
118
+ end
119
+
120
+ # Compare directories as text, not as filesystem objects: repeated and
121
+ # trailing separators are folded away so `-I/usr/include/` matches
122
+ # `/usr/include`, while symlinks are deliberately left unresolved (a
123
+ # .pc's prefix is frequently a symlink, and resolving it would make the
124
+ # output depend on the state of the filesystem).
125
+ def normalize(dir)
126
+ folded = dir.gsub(%r{/{2,}}, "/")
127
+ folded.length > 1 ? folded.sub(%r{/\z}, "") : folded
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../front/ast"
4
+ require_relative "../type"
5
+ require_relative "../compile_error"
6
+
7
+ module Rubycc
8
+ module Preprocess
9
+ # Parses the controlling constant-expression of a "#if"/"#elif" (ISO C
10
+ # 6.10.1) into a Front::AST the shared ConstantEvaluator can fold. It runs
11
+ # over an already-neutralized Front::Token stream: "defined" has been folded
12
+ # to 1/0, macros have been expanded, and every surviving identifier has been
13
+ # turned into the integer 0 (6.10.1p4), so the only leaves that reach here
14
+ # are integer/character constants and parentheses.
15
+ #
16
+ # The grammar admitted is exactly the integer constant-expression subset the
17
+ # preprocessor allows: unary "+ - ~ !", the binary arithmetic, shift,
18
+ # relational, equality and bitwise operators, "&&"/"||", and "?:". Cast,
19
+ # sizeof, comma and assignment are outside it; those spellings either arrive
20
+ # as a stray punctuator (comma, assignment) or, being keyword-spelled
21
+ # identifiers, have already collapsed to 0, and any leftover token is
22
+ # rejected. Rather than a method per precedence level, a single precedence
23
+ # table drives one climbing loop, so the whole binary grammar is one method.
24
+ class ConstantExpressionParser
25
+ # Each binary operator's binding power (higher binds tighter) and the
26
+ # AST::Binary op it lowers to; "&&" and "||" carry :logical markers since
27
+ # they build their own short-circuiting nodes rather than an AST::Binary.
28
+ BINARY_OPERATORS = {
29
+ "*" => [11, :mul], "/" => [11, :div], "%" => [11, :mod],
30
+ "+" => [10, :add], "-" => [10, :sub],
31
+ "<<" => [9, :shl], ">>" => [9, :shr],
32
+ "<" => [8, :lt], ">" => [8, :gt], "<=" => [8, :le], ">=" => [8, :ge],
33
+ "==" => [7, :eq], "!=" => [7, :ne],
34
+ "&" => [6, :and],
35
+ "^" => [5, :xor],
36
+ "|" => [4, :or],
37
+ "&&" => [3, :logical_and],
38
+ "||" => [2, :logical_or]
39
+ }.freeze
40
+
41
+ # The lowest binding power in the table ("||"); the conditional operator
42
+ # sits just below it, so climbing from here collects every binary operator
43
+ # before "?:" is considered.
44
+ LOGICAL_OR_PRECEDENCE = 2
45
+
46
+ # The recursion-depth ceiling, mirroring the main parser's guard: a hostile
47
+ # "#if (((...)))" or "#if !!!...1" would otherwise recurse until the Ruby
48
+ # stack overflows with a bare SystemStackError. This grammar is far lighter
49
+ # than the main parser's (a single precedence-climbing loop rather than a
50
+ # deep chain), so its stack gives out only around 2500 nested parentheses;
51
+ # 500 stops a pathological input after roughly 250 levels — an order of
52
+ # magnitude below that, and far above the trivial nesting any real
53
+ # controlling expression uses (well under ten).
54
+ MAX_NESTING_DEPTH = 500
55
+
56
+ def initialize(tokens, directive)
57
+ @tokens = tokens
58
+ @pos = 0
59
+ # The directive keyword ("if"/"elif"), used only to name diagnostics.
60
+ @directive = directive
61
+ # Live recursion depth, capped at MAX_NESTING_DEPTH by #with_nesting_guard.
62
+ @depth = 0
63
+ end
64
+
65
+ def parse
66
+ node = parse_conditional
67
+ unless current.eof?
68
+ raise_at(current, "extra tokens at end of ##{@directive} expression")
69
+ end
70
+
71
+ node
72
+ end
73
+
74
+ private
75
+
76
+ # conditional-expression: a logical-OR expression optionally followed by
77
+ # "? expression : conditional-expression". Right associativity falls out of
78
+ # recursing on the else arm. Every parenthesized sub-expression re-enters
79
+ # here (a "(" primary recurses back into #parse_conditional), so guarding
80
+ # this entry bounds the depth of nested parentheses and "?:" chains alike.
81
+ def parse_conditional
82
+ with_nesting_guard do
83
+ condition = parse_binary(LOGICAL_OR_PRECEDENCE)
84
+ if current.punct?("?")
85
+ question = advance
86
+ then_expr = parse_conditional
87
+ expect(":", "expected ':' in preprocessor conditional expression")
88
+ else_expr = parse_conditional
89
+ Front::AST::Conditional.new(condition: condition, then_expr: then_expr,
90
+ else_expr: else_expr, token: question)
91
+ else
92
+ condition
93
+ end
94
+ end
95
+ end
96
+
97
+ # Precedence climbing: parse a unary operand, then keep folding in any
98
+ # binary operator whose binding power meets `min_power`, recursing one
99
+ # level tighter on the right so equal-precedence operators associate left.
100
+ def parse_binary(min_power)
101
+ left = parse_unary
102
+ loop do
103
+ info = current.type == :punct ? BINARY_OPERATORS[current.value] : nil
104
+ break if info.nil? || info[0] < min_power
105
+
106
+ power, op = info
107
+ operator = advance
108
+ right = parse_binary(power + 1)
109
+ left = build_binary(op, left, right, operator)
110
+ end
111
+ left
112
+ end
113
+
114
+ def build_binary(op, left, right, token)
115
+ case op
116
+ when :logical_and
117
+ Front::AST::LogicalAnd.new(lhs: left, rhs: right, token: token)
118
+ when :logical_or
119
+ Front::AST::LogicalOr.new(lhs: left, rhs: right, token: token)
120
+ else
121
+ Front::AST::Binary.new(op: op, lhs: left, rhs: right, token: token)
122
+ end
123
+ end
124
+
125
+ # unary-expression: a chain of "+ - ~ !" prefixes over a primary. Unary "+"
126
+ # folds away; "~x" desugars to "x ^ -1", the same lowering the main parser
127
+ # uses so the evaluator meets a single :xor form. A prefix chain recurses
128
+ # here without passing through #parse_conditional, so it carries its own
129
+ # depth guard against a long "!!!...1"/"~~~...1" run.
130
+ def parse_unary
131
+ with_nesting_guard do
132
+ if current.punct?("+")
133
+ advance
134
+ parse_unary
135
+ elsif current.punct?("-")
136
+ token = advance
137
+ Front::AST::Unary.new(op: :neg, operand: parse_unary, token: token)
138
+ elsif current.punct?("!")
139
+ token = advance
140
+ Front::AST::Unary.new(op: :not, operand: parse_unary, token: token)
141
+ elsif current.punct?("~")
142
+ token = advance
143
+ operand = parse_unary
144
+ Front::AST::Binary.new(op: :xor, lhs: operand, rhs: int_literal(-1, token), token: token)
145
+ else
146
+ parse_primary
147
+ end
148
+ end
149
+ end
150
+
151
+ # primary-expression: an integer/character constant, or a parenthesized
152
+ # conditional-expression. Anything else — a leftover operator, a comma, an
153
+ # unmatched paren — is not valid in a preprocessor expression.
154
+ def parse_primary
155
+ token = current
156
+ if token.type == :num
157
+ advance
158
+ int_literal(token.value, token)
159
+ elsif token.punct?("(")
160
+ advance
161
+ node = parse_conditional
162
+ expect(")", "expected ')' in preprocessor expression")
163
+ node
164
+ else
165
+ raise_at(token, "token is not valid in preprocessor expressions")
166
+ end
167
+ end
168
+
169
+ # An integer leaf. The value is what the evaluator reads; the type is
170
+ # nominal (this subset folds every intermediate as an unbounded integer),
171
+ # so a single wide signed type stands in for the intmax the preprocessor
172
+ # nominally computes in.
173
+ def int_literal(value, token)
174
+ Front::AST::IntLit.new(value: value, token: token, type: Type::Long)
175
+ end
176
+
177
+ # Runs `block` one level deeper, rejecting a pathologically nested
178
+ # controlling expression with a located diagnostic (rather than letting it
179
+ # overflow the Ruby stack) once MAX_NESTING_DEPTH levels are live. The
180
+ # depth is decremented in an ensure so a parse error still unwinds it.
181
+ def with_nesting_guard
182
+ if @depth >= MAX_NESTING_DEPTH
183
+ raise_at(current, "##{@directive} expression nested too deeply")
184
+ end
185
+
186
+ @depth += 1
187
+ begin
188
+ yield
189
+ ensure
190
+ @depth -= 1
191
+ end
192
+ end
193
+
194
+ def current
195
+ @tokens[@pos]
196
+ end
197
+
198
+ def advance
199
+ token = @tokens[@pos]
200
+ @pos += 1 unless token.eof?
201
+ token
202
+ end
203
+
204
+ def expect(punct, message)
205
+ raise_at(current, message) unless current.punct?(punct)
206
+
207
+ advance
208
+ end
209
+
210
+ def raise_at(token, description)
211
+ raise CompileError.new(
212
+ description,
213
+ filename: token.filename, line: token.line, column: token.column,
214
+ source_line: token.source_line
215
+ )
216
+ end
217
+ end
218
+ end
219
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../objfile/elf_reader"
4
+ require_relative "../link/errors"
5
+ require_relative "../link/library_resolver"
6
+
7
+ module Rubycc
8
+ module Preprocess
9
+ # Measures the glibc minor version of the C library a compile will link
10
+ # against, so <features.h>'s __GLIBC__/__GLIBC_MINOR__ can describe *this*
11
+ # host instead of the machine the bundled headers were written on.
12
+ #
13
+ # rubycc ships one copy of the libc headers for every host (R8), so the
14
+ # version those headers report used to be a constant: 39, the reference
15
+ # platform's. On any other host that constant made version-gated header code
16
+ # ("#if __GLIBC_PREREQ (2, 38)") select a different branch than the host's
17
+ # own gcc does, and in the over-reporting direction — claiming 2.39 on a
18
+ # 2.34 host — the selected branch can reach for a symbol that libc does not
19
+ # have, which surfaces as an undefined symbol at link or dlopen time
20
+ # (docs/development/GAPS.md gap U). The value therefore has to be measured, and the only
21
+ # honest place to measure it is the C library itself: not the host's
22
+ # /usr/include (a distroless target has none, which is the whole reason the
23
+ # bundled headers exist) and not the host's gcc (rubycc must work without
24
+ # one).
25
+ #
26
+ # What a glibc image states about itself is the set of symbol versions it
27
+ # defines — GLIBC_2.2.5, GLIBC_2.3, ... GLIBC_2.39 — recorded in its
28
+ # .gnu.version_d table, which ELFReader reads. The highest GLIBC_2.<n> it
29
+ # defines is the newest interface it can satisfy, so <n> is what
30
+ # __GLIBC_MINOR__ has to say for a version gate to pick a branch this libc
31
+ # can actually back.
32
+ #
33
+ # That measure has one known way of coming out low: a glibc release that
34
+ # added no new symbol to libc.so.6 gets no version node of its own (2.19,
35
+ # 2.20, 2.21 and 2.37 are the releases where this happened), so on such a
36
+ # host the highest node names the previous release. The result is then
37
+ # conservative rather than wrong in the dangerous direction — a gate may
38
+ # take an older branch, never one whose symbols are missing — but it does
39
+ # differ from what the host's own headers say. Reading the version out of
40
+ # glibc's banner string instead would be exact; it is not done here because
41
+ # that means pattern-matching English prose in .rodata, whereas the version
42
+ # table is a structure the ABI defines.
43
+ #
44
+ # Which file is "the C library" is a question about the *target*, never
45
+ # about this host's own paths: the search runs through LibraryResolver with
46
+ # the target's own system directories, exactly as the `-lc` of a real link
47
+ # would, so a cross compile measures the cross libc and a host with no libc
48
+ # at all measures nothing and falls back (see .minor_for).
49
+ module GlibcVersion
50
+ # The library request that finds the C library. Deliberately the same
51
+ # spelling a link uses, so this reads whatever `-lc` would reach —
52
+ # including through the GNU-ld script glibc installs as libc.so.
53
+ LIBC_LIBRARY = "c"
54
+
55
+ # A version node naming a whole glibc release: "GLIBC_2.39" and kin. The
56
+ # three-component names ("GLIBC_2.2.5") and the non-release ones
57
+ # ("GLIBC_PRIVATE", "GLIBC_ABI_DT_RELR") are not releases whose minor
58
+ # number __GLIBC_MINOR__ could carry, so only this shape counts. The major
59
+ # number is pinned at 2 for the same reason __GLIBC__ is: glibc has
60
+ # carried it since 1997, and a 3.x would be a different ABI whose minor
61
+ # number this measurement should not be feeding to a 2.x header anyway.
62
+ # Naming glibc's own spelling here is not the hard-coded host identity
63
+ # test/test_platform_literals.rb guards against: this module measures
64
+ # glibc specifically, and a libc that spells its versions otherwise is
65
+ # precisely the "not measurable" case (see .minor_in).
66
+ RELEASE_VERSION = /\AGLIBC_2\.(\d+)\z/
67
+
68
+ class << self
69
+ # The measured glibc minor version for `target` (an architecture name or
70
+ # triple, as LibraryResolver takes), or nil when this host has no glibc
71
+ # to measure — no C library on the target's search path, a libc that is
72
+ # not glibc (musl defines no version nodes), or one this reader cannot
73
+ # parse. The caller decides what an unmeasurable host means; the
74
+ # preprocessor falls back to the value the bundled <features.h> carries.
75
+ #
76
+ # Memoized per target for the life of the process: a translation unit
77
+ # must not pay a 2 MB read of libc for a value that cannot change while
78
+ # the compiler runs (N1). nil is memoized too, so a host without a libc
79
+ # searches once rather than once per unit.
80
+ def minor_for(target)
81
+ cache = (@minor_for ||= {})
82
+ return cache[target] if cache.key?(target)
83
+
84
+ cache[target] = measure(target)
85
+ end
86
+
87
+ # The uncached measurement, kept separate so a caller (and the tests)
88
+ # can ask for a fresh reading. The first library that states a version
89
+ # settles it: the `-lc` resolution yields libc itself ahead of the
90
+ # loader it names, and a glibc loader repeats libc's versions rather
91
+ # than adding to them.
92
+ def measure(target)
93
+ libc_images(target).each do |path|
94
+ minor = minor_in(path)
95
+ return minor if minor
96
+ end
97
+ nil
98
+ end
99
+
100
+ # The highest glibc release version the shared object at `path` defines,
101
+ # or nil when it defines none — which is the answer for a musl libc, for
102
+ # a relocatable object, and for anything that is not an ELF file this
103
+ # reader accepts. An unreadable file is likewise "cannot measure", not
104
+ # an error: this runs on the way to a compile that must still happen.
105
+ def minor_in(path)
106
+ reader = ObjFile::ELFReader.read_file(path)
107
+ reader.version_definitions.filter_map { |v| v.name[RELEASE_VERSION, 1]&.to_i }.max
108
+ rescue ObjFile::ELFFormatError, SystemCallError, IOError
109
+ nil
110
+ end
111
+
112
+ # The shared objects a `-lc` link for `target` would bind against, in
113
+ # the order the linker sees them. A target with no C library on its
114
+ # search path is not an error here (a cross target whose sysroot is not
115
+ # installed, a musl image with no glibc): it simply cannot be measured.
116
+ def libc_images(target)
117
+ Link::LibraryResolver.resolve([LIBC_LIBRARY], target: target).needed
118
+ rescue Link::LinkError, SystemCallError
119
+ []
120
+ end
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubycc
4
+ module Preprocess
5
+ # A preprocessing token (ISO C 6.4). Unlike a Front::Token it holds only the
6
+ # source spelling (`text`), deferring interpretation of numbers, literals and
7
+ # keywords to the conversion that ends translation phase 4. `type` is one of:
8
+ #
9
+ # :identifier an identifier or keyword spelling
10
+ # :pp_number a preprocessing number (6.4.8), broader than a C constant
11
+ # :string a string literal, quotes and escapes still verbatim
12
+ # :char a character constant, quotes and escapes still verbatim
13
+ # :punct a punctuator (including the pp-only "#" and "##")
14
+ # :newline an end-of-line marker kept so directives stay line-oriented
15
+ # :other any single character matching no other category (6.4p1)
16
+ # :eof the end of the translation unit
17
+ #
18
+ # The location fields follow the same 1-based line/column convention as
19
+ # Front::Token, and point at the token's first character on its physical
20
+ # line (a token spliced across a line continuation keeps its start line).
21
+ # `space_before` records whether whitespace or a comment preceded the token,
22
+ # which the directive layer consults to classify a macro definition.
23
+ # `suppress` is the token's own set of macro names it must not be re-expanded
24
+ # as (6.10.3.4, "painted blue"): a frozen list carried along every copy, so
25
+ # each token remembers the expansions it was already born from.
26
+ class PPToken
27
+ # The suppression set of a token that came straight from source and has yet
28
+ # to pass through any macro; shared so unpainted tokens cost no allocation.
29
+ NO_SUPPRESS = [].freeze
30
+
31
+ attr_reader :type, :text, :filename, :line, :column, :source_line, :space_before, :suppress
32
+
33
+ def initialize(type:, text:, filename:, line:, column:, source_line:, space_before: false,
34
+ suppress: NO_SUPPRESS)
35
+ @type = type
36
+ @text = text
37
+ @filename = filename
38
+ @line = line
39
+ @column = column
40
+ @source_line = source_line
41
+ @space_before = space_before
42
+ @suppress = suppress
43
+ end
44
+
45
+ def newline?
46
+ @type == :newline
47
+ end
48
+
49
+ def eof?
50
+ @type == :eof
51
+ end
52
+
53
+ def punct?(str)
54
+ @type == :punct && @text == str
55
+ end
56
+
57
+ def inspect
58
+ "#<PPToken #{type} #{text.inspect} @#{line}:#{column}>"
59
+ end
60
+ end
61
+ end
62
+ end