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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +56 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +52 -0
- data/README.md +208 -0
- data/data/README.md +117 -0
- data/data/r10_corpus_scan.json +4082 -0
- data/data/r10_manual_classification.json +5342 -0
- data/data/r10_verification_m4a.json +1531 -0
- data/data/r10_verification_m4b.json +1818 -0
- data/data/r10_verification_m4c.json +1489 -0
- data/data/r10_verification_m4d.json +318 -0
- data/data/r10_verification_rbs.json +192 -0
- data/data/verified_gems.json +397 -0
- data/exe/rmake +16 -0
- data/exe/rubycc +9 -0
- data/exe/rubycc-ar +114 -0
- data/exe/rubycc-doctor +14 -0
- data/exe/rubycc-pkgconf +8 -0
- data/include/float.h +87 -0
- data/include/iso646.h +18 -0
- data/include/libc/alloca.h +18 -0
- data/include/libc/arpa/inet.h +61 -0
- data/include/libc/assert.h +43 -0
- data/include/libc/dirent.h +72 -0
- data/include/libc/dlfcn.h +58 -0
- data/include/libc/features.h +205 -0
- data/include/libc/glibc/aarch64/ctype.h +108 -0
- data/include/libc/glibc/aarch64/endian.h +54 -0
- data/include/libc/glibc/aarch64/errno.h +154 -0
- data/include/libc/glibc/aarch64/fcntl.h +144 -0
- data/include/libc/glibc/aarch64/inttypes.h +182 -0
- data/include/libc/glibc/aarch64/limits.h +63 -0
- data/include/libc/glibc/aarch64/pthread.h +131 -0
- data/include/libc/glibc/aarch64/setjmp.h +84 -0
- data/include/libc/glibc/aarch64/stdint.h +174 -0
- data/include/libc/glibc/aarch64/sys/epoll.h +88 -0
- data/include/libc/glibc/aarch64/sys/fcntl.h +19 -0
- data/include/libc/glibc/aarch64/sys/select.h +75 -0
- data/include/libc/glibc/aarch64/sys/stat.h +136 -0
- data/include/libc/glibc/aarch64/sys/syscall.h +167 -0
- data/include/libc/glibc/aarch64/sys/time.h +74 -0
- data/include/libc/glibc/aarch64/sys/types.h +127 -0
- data/include/libc/glibc/aarch64/time.h +113 -0
- data/include/libc/glibc/x86_64/ctype.h +108 -0
- data/include/libc/glibc/x86_64/endian.h +54 -0
- data/include/libc/glibc/x86_64/errno.h +154 -0
- data/include/libc/glibc/x86_64/fcntl.h +134 -0
- data/include/libc/glibc/x86_64/inttypes.h +182 -0
- data/include/libc/glibc/x86_64/limits.h +61 -0
- data/include/libc/glibc/x86_64/pthread.h +111 -0
- data/include/libc/glibc/x86_64/setjmp.h +82 -0
- data/include/libc/glibc/x86_64/stdint.h +163 -0
- data/include/libc/glibc/x86_64/sys/epoll.h +91 -0
- data/include/libc/glibc/x86_64/sys/fcntl.h +19 -0
- data/include/libc/glibc/x86_64/sys/select.h +75 -0
- data/include/libc/glibc/x86_64/sys/stat.h +130 -0
- data/include/libc/glibc/x86_64/sys/syscall.h +188 -0
- data/include/libc/glibc/x86_64/sys/time.h +74 -0
- data/include/libc/glibc/x86_64/sys/types.h +123 -0
- data/include/libc/glibc/x86_64/time.h +113 -0
- data/include/libc/grp.h +50 -0
- data/include/libc/langinfo.h +126 -0
- data/include/libc/link.h +18 -0
- data/include/libc/locale.h +83 -0
- data/include/libc/math.h +191 -0
- data/include/libc/netinet/in.h +126 -0
- data/include/libc/netinet/tcp.h +44 -0
- data/include/libc/poll.h +46 -0
- data/include/libc/pwd.h +58 -0
- data/include/libc/regex.h +53 -0
- data/include/libc/sched.h +35 -0
- data/include/libc/signal.h +204 -0
- data/include/libc/stdio.h +157 -0
- data/include/libc/stdlib.h +92 -0
- data/include/libc/string.h +74 -0
- data/include/libc/strings.h +25 -0
- data/include/libc/sys/cdefs.h +126 -0
- data/include/libc/sys/inotify.h +111 -0
- data/include/libc/sys/ioctl.h +36 -0
- data/include/libc/sys/mman.h +65 -0
- data/include/libc/sys/param.h +41 -0
- data/include/libc/sys/resource.h +109 -0
- data/include/libc/sys/socket.h +167 -0
- data/include/libc/sys/statfs.h +82 -0
- data/include/libc/sys/timerfd.h +55 -0
- data/include/libc/sys/uio.h +40 -0
- data/include/libc/sys/un.h +25 -0
- data/include/libc/sys/utsname.h +35 -0
- data/include/libc/sys/wait.h +135 -0
- data/include/libc/termios.h +179 -0
- data/include/libc/unistd.h +194 -0
- data/include/stdalign.h +16 -0
- data/include/stdarg.h +31 -0
- data/include/stdatomic.h +158 -0
- data/include/stdbool.h +15 -0
- data/include/stdckdint.h +28 -0
- data/include/stddef.h +60 -0
- data/include/stdnoreturn.h +18 -0
- data/include/x86intrin.h +16 -0
- data/lib/rubycc/backend/aarch64.rb +1724 -0
- data/lib/rubycc/backend/x86_64.rb +1369 -0
- data/lib/rubycc/compile_error.rb +36 -0
- data/lib/rubycc/compiler.rb +305 -0
- data/lib/rubycc/doctor/builder.rb +151 -0
- data/lib/rubycc/doctor/cli.rb +190 -0
- data/lib/rubycc/doctor/fetcher.rb +84 -0
- data/lib/rubycc/doctor/gemfile.rb +115 -0
- data/lib/rubycc/doctor/verified_gems.rb +114 -0
- data/lib/rubycc/doctor.rb +16 -0
- data/lib/rubycc/driver.rb +463 -0
- data/lib/rubycc/front/ast.rb +528 -0
- data/lib/rubycc/front/constant_evaluator.rb +631 -0
- data/lib/rubycc/front/initializer_resolver.rb +592 -0
- data/lib/rubycc/front/lexeme_reader.rb +460 -0
- data/lib/rubycc/front/lexer.rb +232 -0
- data/lib/rubycc/front/parser.rb +4122 -0
- data/lib/rubycc/front/token.rb +49 -0
- data/lib/rubycc/ir/call_convention.rb +486 -0
- data/lib/rubycc/ir/generator.rb +6036 -0
- data/lib/rubycc/ir/ir.rb +417 -0
- data/lib/rubycc/link/compat_runtime.rb +89 -0
- data/lib/rubycc/link/errors.rb +13 -0
- data/lib/rubycc/link/executable_linker.rb +405 -0
- data/lib/rubycc/link/library_resolver.rb +437 -0
- data/lib/rubycc/link/partial_linker.rb +546 -0
- data/lib/rubycc/link/shared_linker.rb +1732 -0
- data/lib/rubycc/mkmf_shim.rb +83 -0
- data/lib/rubycc/objfile/ar_archive.rb +437 -0
- data/lib/rubycc/objfile/elf_reader.rb +646 -0
- data/lib/rubycc/objfile/elf_writer.rb +891 -0
- data/lib/rubycc/objfile/relocatable_writer.rb +376 -0
- data/lib/rubycc/pkgconf/cli.rb +139 -0
- data/lib/rubycc/pkgconf/errors.rb +39 -0
- data/lib/rubycc/pkgconf/model.rb +28 -0
- data/lib/rubycc/pkgconf/parser.rb +109 -0
- data/lib/rubycc/pkgconf/pkgconf.rb +15 -0
- data/lib/rubycc/pkgconf/resolver.rb +70 -0
- data/lib/rubycc/pkgconf/search_path.rb +38 -0
- data/lib/rubycc/pkgconf/system_path_filter.rb +131 -0
- data/lib/rubycc/preprocess/constant_expression.rb +219 -0
- data/lib/rubycc/preprocess/glibc_version.rb +124 -0
- data/lib/rubycc/preprocess/pp_token.rb +62 -0
- data/lib/rubycc/preprocess/preprocessor.rb +2020 -0
- data/lib/rubycc/preprocess/scanner.rb +290 -0
- data/lib/rubycc/preprocess/token_converter.rb +157 -0
- data/lib/rubycc/rmake/cli.rb +143 -0
- data/lib/rubycc/rmake/errors.rb +71 -0
- data/lib/rubycc/rmake/executor.rb +818 -0
- data/lib/rubycc/rmake/expander.rb +251 -0
- data/lib/rubycc/rmake/makefile.rb +352 -0
- data/lib/rubycc/rmake/model.rb +137 -0
- data/lib/rubycc/rmake/parser.rb +226 -0
- data/lib/rubycc/rmake/rmake.rb +15 -0
- data/lib/rubycc/type.rb +1236 -0
- data/lib/rubycc/version.rb +5 -0
- data/lib/rubycc.rb +36 -0
- data/lib/rubygems_plugin.rb +102 -0
- 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
|