rubycc 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +61 -0
- data/README.md +26 -14
- data/data/verified_gems.json +85 -63
- data/exe/rubycc-ar +11 -3
- data/include/libc/sys/cdefs.h +12 -0
- data/lib/rubycc/backend/aarch64.rb +705 -117
- data/lib/rubycc/backend/slot_residency.rb +169 -0
- data/lib/rubycc/backend/x86_64.rb +924 -137
- data/lib/rubycc/command_line.rb +339 -0
- data/lib/rubycc/compile_error.rb +6 -3
- data/lib/rubycc/compiler.rb +17 -2
- data/lib/rubycc/diagnostics.rb +105 -0
- data/lib/rubycc/doctor/gemfile.rb +12 -3
- data/lib/rubycc/doctor/verified_gems.rb +5 -1
- data/lib/rubycc/driver.rb +66 -10
- data/lib/rubycc/front/ast.rb +18 -7
- data/lib/rubycc/front/constant_evaluator.rb +12 -0
- data/lib/rubycc/front/lexeme_reader.rb +3 -1
- data/lib/rubycc/front/parser.rb +51 -18
- data/lib/rubycc/ir/analysis.rb +82 -0
- data/lib/rubycc/ir/call_convention.rb +74 -7
- data/lib/rubycc/ir/generator.rb +319 -9
- data/lib/rubycc/ir/ir.rb +39 -1
- data/lib/rubycc/ir/promotion.rb +255 -0
- data/lib/rubycc/ir/simplify.rb +570 -0
- data/lib/rubycc/link/library_resolver.rb +17 -5
- data/lib/rubycc/link/partial_linker.rb +8 -1
- data/lib/rubycc/link/shared_linker.rb +2 -2
- data/lib/rubycc/mkmf_shim.rb +178 -12
- data/lib/rubycc/objfile/ar_archive.rb +13 -2
- data/lib/rubycc/objfile/elf_reader.rb +13 -2
- data/lib/rubycc/pkgconf/parser.rb +4 -0
- data/lib/rubycc/pkgconf/resolver.rb +3 -1
- data/lib/rubycc/pkgconf/system_path_filter.rb +8 -2
- data/lib/rubycc/preprocess/preprocessor.rb +161 -37
- data/lib/rubycc/preprocess/scanner.rb +69 -14
- data/lib/rubycc/preprocess/token_converter.rb +11 -1
- data/lib/rubycc/rmake/cli.rb +32 -4
- data/lib/rubycc/rmake/executor.rb +157 -226
- data/lib/rubycc/rmake/makefile.rb +35 -13
- data/lib/rubycc/rmake/parser.rb +8 -2
- data/lib/rubycc/rmake/rmake.rb +1 -0
- data/lib/rubycc/rmake/tool_command.rb +69 -0
- data/lib/rubycc/shell.rb +510 -0
- data/lib/rubycc/type.rb +23 -7
- data/lib/rubycc/version.rb +1 -1
- data/lib/rubycc.rb +12 -0
- data/lib/rubygems_plugin.rb +31 -3
- metadata +14 -3
data/lib/rubycc/mkmf_shim.rb
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "rbconfig"
|
|
4
|
+
require_relative "command_line"
|
|
4
5
|
|
|
5
6
|
# The mkmf integration shim: requiring this file (before `require "mkmf"`)
|
|
6
7
|
# reroutes every conftest command mkmf issues — compile, link, preprocess — to
|
|
@@ -20,31 +21,195 @@ require "rbconfig"
|
|
|
20
21
|
# in-process only (no file on disk is modified) and idempotent: requiring the
|
|
21
22
|
# shim twice is a no-op.
|
|
22
23
|
#
|
|
23
|
-
# The rubycc executables
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
24
|
+
# The rubycc executables are launched through the running interpreter — the
|
|
25
|
+
# rewritten `CC` is `<RbConfig.ruby> <path>/exe/rubycc`, not the bare path. Their
|
|
26
|
+
# `#!/usr/bin/env ruby` shebang is resolved by the kernel, and a minimal image
|
|
27
|
+
# (DESIGN R5) need not have /usr/bin/env: `dhi.io/ruby:4` has no coreutils at
|
|
28
|
+
# all, and there every exec of the executable fails with status 127 before a line
|
|
29
|
+
# of rubycc runs. Naming the interpreter also settles *which* Ruby compiles —
|
|
30
|
+
# `env ruby` takes the first one on PATH, which on a multi-Ruby host is not
|
|
31
|
+
# necessarily the one the gem is being installed into.
|
|
32
|
+
#
|
|
33
|
+
# They also `require "rubycc"`, which needs the repository's `lib/` on the load
|
|
34
|
+
# path when rubycc is run from a checkout rather than an installed gem. The shim
|
|
35
|
+
# prepends that directory to `RUBYLIB` so the value propagates to the child
|
|
36
|
+
# processes mkmf spawns.
|
|
37
|
+
#
|
|
38
|
+
# Rewriting the RbConfig keys settles *what* mkmf runs; ShellFreeCommands
|
|
39
|
+
# settles *how*. mkmf builds each conftest command as one string and hands it to
|
|
40
|
+
# `system`/`IO.popen`, which lets Ruby decide between exec'ing it and handing it
|
|
41
|
+
# to /bin/sh — and the environment DESIGN R5 targets has no /bin/sh. The shim
|
|
42
|
+
# prepends the two methods that spawn (`xsystem`, `xpopen`) and converts a string
|
|
43
|
+
# command into an argv array first, so nothing about the run depends on a shell
|
|
44
|
+
# being there. A command that genuinely needs shell interpretation is refused
|
|
45
|
+
# with its reason written to mkmf.log rather than being run through a shell that
|
|
46
|
+
# may or may not exist.
|
|
27
47
|
module Rubycc
|
|
28
48
|
module MkmfShim
|
|
29
49
|
# The repository's lib/ directory (this file lives at lib/rubycc/mkmf_shim.rb)
|
|
30
50
|
# and the gcc-compatible executables that stand in for the external toolchain.
|
|
31
51
|
LIB_DIR = File.expand_path("..", __dir__)
|
|
32
|
-
|
|
33
|
-
|
|
52
|
+
EXE_DIR = File.expand_path("../../exe", __dir__)
|
|
53
|
+
RUBYCC_EXE = File.join(EXE_DIR, "rubycc")
|
|
54
|
+
PKGCONF_EXE = File.join(EXE_DIR, "rubycc-pkgconf")
|
|
55
|
+
|
|
56
|
+
# The interpreter every rubycc executable is launched through: this very
|
|
57
|
+
# process's Ruby, by absolute path. RbConfig.ruby is what mkmf, RubyGems and
|
|
58
|
+
# rubycc's own doctor already use to re-enter Ruby.
|
|
59
|
+
RUBY = RbConfig.ruby
|
|
60
|
+
|
|
61
|
+
# A command mkmf can embed verbatim, built from +words+: the interpreter
|
|
62
|
+
# first, then the executable (see the file comment — the shebang is not
|
|
63
|
+
# resolvable everywhere). Each word is quoted, because a command that has to
|
|
64
|
+
# travel as a single string comes apart at the first space in an
|
|
65
|
+
# installation path, and both splitters that read it back — rubycc's own and
|
|
66
|
+
# the Shellwords RubyGems uses — honour the same quoting.
|
|
67
|
+
def self.launcher(*words)
|
|
68
|
+
words.map { |word| CommandLine.quote(word) }.join(" ")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The rubycc compiler as mkmf must spell it.
|
|
72
|
+
RUBYCC_COMMAND = launcher(RUBY, RUBYCC_EXE)
|
|
34
73
|
|
|
35
74
|
# The RbConfig keys rewritten to point at rubycc. Each maps to the command
|
|
36
|
-
# string mkmf embeds verbatim when it expands `$(CC)` and friends:
|
|
37
|
-
#
|
|
38
|
-
# driver's own mode flag) so mkmf's `system(env,
|
|
75
|
+
# string mkmf embeds verbatim when it expands `$(CC)` and friends: shell-
|
|
76
|
+
# metacharacter-free words (the interpreter, the executable and, for the
|
|
77
|
+
# compound tools, the driver's own mode flag) so mkmf's `system(env,
|
|
78
|
+
# *command)` execs them directly.
|
|
79
|
+
#
|
|
80
|
+
# PKG_CONFIG is the exception that stays a bare path: mkmf resolves it with
|
|
81
|
+
# `find_executable0` (mkmf.rb's pkg_config) before running it, and that stats
|
|
82
|
+
# the value as one file name — a two-word command would simply not be found
|
|
83
|
+
# and pkg-config support would silently switch itself off. The interpreter is
|
|
84
|
+
# put in front of it at spawn time instead, by ShellFreeCommands.
|
|
39
85
|
def self.replacements
|
|
40
86
|
{
|
|
41
|
-
"CC" =>
|
|
42
|
-
"LDSHARED" => "#{
|
|
43
|
-
"CPP" => "#{
|
|
87
|
+
"CC" => RUBYCC_COMMAND,
|
|
88
|
+
"LDSHARED" => "#{RUBYCC_COMMAND} -shared",
|
|
89
|
+
"CPP" => "#{RUBYCC_COMMAND} -E",
|
|
44
90
|
"PKG_CONFIG" => PKGCONF_EXE
|
|
45
91
|
}
|
|
46
92
|
end
|
|
47
93
|
|
|
94
|
+
# Raised when a conftest command cannot be run without a shell. It is a
|
|
95
|
+
# refusal, not a build failure: mkmf reads a false return as "the compiler
|
|
96
|
+
# cannot build this", which would misreport an unrunnable command as a
|
|
97
|
+
# missing header or a broken toolchain, so the shim stops the extconf
|
|
98
|
+
# instead.
|
|
99
|
+
class ShellRequiredError < Rubycc::Error; end
|
|
100
|
+
|
|
101
|
+
# Prepended to MakeMakefile so the two methods that spawn a conftest —
|
|
102
|
+
# `xsystem` (compile/link/run) and `xpopen` (the same, reading the output) —
|
|
103
|
+
# receive an argv array where mkmf built a string. mkmf's own
|
|
104
|
+
# `expand_command` keeps whatever shape it is given (an Array stays an Array
|
|
105
|
+
# and is spawned directly; a String stays a String and reaches the shell),
|
|
106
|
+
# so converting the command on the way in is the whole change: the logging,
|
|
107
|
+
# the werror handling and the env hash are still mkmf's.
|
|
108
|
+
#
|
|
109
|
+
# The conversion runs mkmf's `$(VAR)` expansion *before* splitting, which is
|
|
110
|
+
# the order a shell saw: mkmf expanded the variables and sh did the word
|
|
111
|
+
# splitting. `super` expands again, on words that no longer hold `$(...)`.
|
|
112
|
+
module ShellFreeCommands
|
|
113
|
+
def xsystem(command, werror: false)
|
|
114
|
+
super(rubycc_spawn_form(command), werror: werror)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def xpopen(command, *mode, &block)
|
|
118
|
+
super(rubycc_spawn_form(command), *mode, &block)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
# The command as something spawnable without a shell. Arrays are already
|
|
124
|
+
# that and only have the interpreter supplied (mkmf builds the pkg-config
|
|
125
|
+
# calls that way); a string is expanded, split and then treated the same.
|
|
126
|
+
# The method name is prefixed because prepending to MakeMakefile puts it on
|
|
127
|
+
# every extconf's Object.
|
|
128
|
+
def rubycc_spawn_form(command)
|
|
129
|
+
return MkmfShim.interpreter_form(command) unless command.is_a?(String)
|
|
130
|
+
|
|
131
|
+
_env, expanded = expand_command(command)
|
|
132
|
+
MkmfShim.spawn_form(expanded)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Split +command+ into the argv array mkmf should spawn, or refuse it.
|
|
137
|
+
# A one-word command comes back as `[[program, program]]`: `system(env, str)`
|
|
138
|
+
# with a single string would put Ruby back in charge of choosing between exec
|
|
139
|
+
# and the shell (it scans for metacharacters and splits on spaces), and the
|
|
140
|
+
# explicit [program, argv0] form takes that choice away — `./conftest` and a
|
|
141
|
+
# path with a space in it are then spawned the same way.
|
|
142
|
+
def self.spawn_form(command)
|
|
143
|
+
argv = interpreter_form(CommandLine.argv(command))
|
|
144
|
+
argv.length == 1 ? [[argv[0], argv[0]]] : argv
|
|
145
|
+
rescue CommandLine::UnsupportedSyntaxError => e
|
|
146
|
+
refuse!(e, command)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# +argv+ with the interpreter put in front when the command runs one of
|
|
150
|
+
# rubycc's own executables directly. The rewritten `CC`/`LDSHARED`/`CPP`
|
|
151
|
+
# already carry it, but `PKG_CONFIG` cannot (see .replacements), and mkmf
|
|
152
|
+
# spawns pkg-config as an array — which reaches the kernel verbatim, shebang
|
|
153
|
+
# line and all. Supplying the interpreter here covers both spellings with one
|
|
154
|
+
# rule. A leading env hash (mkmf passes PKG_CONFIG_PATH that way) is skipped
|
|
155
|
+
# over rather than counted as the program word.
|
|
156
|
+
def self.interpreter_form(argv)
|
|
157
|
+
return argv unless argv.is_a?(Array)
|
|
158
|
+
|
|
159
|
+
index = argv.index { |word| !word.is_a?(Hash) }
|
|
160
|
+
return argv if index.nil? || !own_executable?(argv[index])
|
|
161
|
+
|
|
162
|
+
argv.dup.insert(index, RUBY)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Whether +word+ names one of the executables this gem ships — the files
|
|
166
|
+
# whose `#!/usr/bin/env ruby` line an exec would have to resolve.
|
|
167
|
+
def self.own_executable?(word)
|
|
168
|
+
word.is_a?(String) && File.dirname(word) == EXE_DIR
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
# Report a command the shim will not run. The reason goes to mkmf.log, where
|
|
172
|
+
# anyone debugging a failed extension build already looks, and then the
|
|
173
|
+
# exception stops the extconf: a silent false here is exactly the
|
|
174
|
+
# misdiagnosis ("You have to install development tools first") that hid this
|
|
175
|
+
# whole problem before.
|
|
176
|
+
def self.refuse!(error, command)
|
|
177
|
+
message = <<~MSG
|
|
178
|
+
rubycc: refusing to run a conftest command that needs a shell (#{error.construct}):
|
|
179
|
+
#{command}
|
|
180
|
+
The mkmf shim spawns conftest commands as an argv array because the target
|
|
181
|
+
environment has no /bin/sh (DESIGN R5), and it does not fall back to one:
|
|
182
|
+
a fallback would make the result depend on whether a shell happens to be
|
|
183
|
+
installed. Nothing was run.
|
|
184
|
+
MSG
|
|
185
|
+
log_to_mkmf(message)
|
|
186
|
+
raise ShellRequiredError, message
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# Append +message+ to mkmf.log through mkmf's own Logging module (which
|
|
190
|
+
# opens the file if it is not open yet). Logging is best-effort: if the log
|
|
191
|
+
# cannot be written the refusal itself must still be raised.
|
|
192
|
+
def self.log_to_mkmf(message)
|
|
193
|
+
return unless defined?(::MakeMakefile::Logging)
|
|
194
|
+
|
|
195
|
+
::MakeMakefile::Logging.message("%s", message)
|
|
196
|
+
rescue SystemCallError, IOError
|
|
197
|
+
nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Put ShellFreeCommands in front of MakeMakefile's own methods.
|
|
201
|
+
#
|
|
202
|
+
# mkmf is normally *not* loaded yet at this point: the shim is injected with
|
|
203
|
+
# `RUBYOPT=-rrubycc/mkmf_shim`, which runs before extconf.rb requires mkmf.
|
|
204
|
+
# Defining the module first and prepending to it works in that order because
|
|
205
|
+
# mkmf reopens the module rather than replacing it, and its closing
|
|
206
|
+
# `include MakeMakefile` carries the prepended module into Object with it.
|
|
207
|
+
# Prepending an already-prepended module is a no-op, so this is idempotent.
|
|
208
|
+
def self.prepend_shell_free_commands!
|
|
209
|
+
Object.const_set(:MakeMakefile, Module.new) unless defined?(::MakeMakefile)
|
|
210
|
+
::MakeMakefile.prepend(ShellFreeCommands)
|
|
211
|
+
end
|
|
212
|
+
|
|
48
213
|
# Rewrites the toolchain keys in both RbConfig hashes and prepends the
|
|
49
214
|
# repository lib/ to RUBYLIB. Idempotent: the RUBYLIB entry is added only
|
|
50
215
|
# once, and re-assigning the same command strings is harmless.
|
|
@@ -65,6 +230,7 @@ module Rubycc
|
|
|
65
230
|
end
|
|
66
231
|
|
|
67
232
|
prepend_rubylib!
|
|
233
|
+
prepend_shell_free_commands!
|
|
68
234
|
end
|
|
69
235
|
|
|
70
236
|
# Puts the repository lib/ at the front of RUBYLIB so a rubycc executable
|
|
@@ -193,6 +193,14 @@ module Rubycc
|
|
|
193
193
|
|
|
194
194
|
# Resolves a `/N` reference: read from byte `offset` in the `//` table up to
|
|
195
195
|
# the terminating `\n` and drop the trailing `/` GNU appends to each entry.
|
|
196
|
+
#
|
|
197
|
+
# The slice is left as bytes, like every other name this reader hands back
|
|
198
|
+
# (lib/rubycc.rb). An archive names a member in whatever bytes the
|
|
199
|
+
# filesystem gave it, and two strings holding the same non-ASCII bytes
|
|
200
|
+
# under different encodings are neither == nor eql? and hash apart — so
|
|
201
|
+
# tagging a name here would make it differ from a SHORT name spelled with
|
|
202
|
+
# the same bytes, and whether a caller's comparison hit would then depend
|
|
203
|
+
# on how long the name happened to be.
|
|
196
204
|
def resolve_long_name(offset, pos)
|
|
197
205
|
table = @name_table or
|
|
198
206
|
raise ArFormatError, "member at offset #{pos} references a name table that is absent"
|
|
@@ -201,7 +209,7 @@ module Rubycc
|
|
|
201
209
|
end
|
|
202
210
|
|
|
203
211
|
stop = table.index("\n".b, offset) || table.bytesize
|
|
204
|
-
table.byteslice(offset...stop).chomp("/")
|
|
212
|
+
table.byteslice(offset...stop).chomp("/")
|
|
205
213
|
end
|
|
206
214
|
|
|
207
215
|
# Turns the raw `/` symbol table into name -> member entries. Deferred until
|
|
@@ -223,7 +231,10 @@ module Rubycc
|
|
|
223
231
|
cursor = 0
|
|
224
232
|
count.times do |i|
|
|
225
233
|
stop = names_blob.index("\0".b, cursor) or break
|
|
226
|
-
name
|
|
234
|
+
# Bytes, like the member names: a symbol name is whatever bytes the
|
|
235
|
+
# member's ELF string table held, and one spelling for every name a
|
|
236
|
+
# reader hands back is the whole point (see resolve_long_name).
|
|
237
|
+
name = names_blob.byteslice(cursor...stop)
|
|
227
238
|
cursor = stop + 1
|
|
228
239
|
member = by_offset[offsets[i]]
|
|
229
240
|
@symbols << { name: name, member: member } if member
|
|
@@ -627,11 +627,22 @@ module Rubycc
|
|
|
627
627
|
# A NUL-terminated string starting at `offset` within a string table's
|
|
628
628
|
# bytes; an out-of-range offset yields "" rather than raising, since a
|
|
629
629
|
# zero sh_name legitimately points at the leading NUL (the empty name).
|
|
630
|
+
#
|
|
631
|
+
# The slice is left as bytes, like everything rubycc reads (lib/rubycc.rb),
|
|
632
|
+
# and this one method is where every name the reader hands back comes from:
|
|
633
|
+
# section names, .symtab / .dynsym symbol names, DT_SONAME / DT_NEEDED, and
|
|
634
|
+
# version names. A string table holds whatever bytes produced the object —
|
|
635
|
+
# a C identifier cannot carry non-ASCII, an assembler label can — and two
|
|
636
|
+
# strings holding the same non-ASCII bytes under different encodings are
|
|
637
|
+
# neither == nor eql? and hash apart, so tagging here would leave a name
|
|
638
|
+
# unfindable through the bytes a caller already holds (#symbol, #section)
|
|
639
|
+
# and spelled differently from the same name read by ArReader, which takes
|
|
640
|
+
# it out of this very string table when it builds an archive's index.
|
|
630
641
|
def read_string(strtab, offset)
|
|
631
|
-
return "" if offset >= strtab.bytesize
|
|
642
|
+
return "".b if offset >= strtab.bytesize
|
|
632
643
|
|
|
633
644
|
stop = strtab.index("\0".b, offset) || strtab.bytesize
|
|
634
|
-
strtab.byteslice(offset...stop)
|
|
645
|
+
strtab.byteslice(offset...stop)
|
|
635
646
|
end
|
|
636
647
|
|
|
637
648
|
# Guards every structural read: the whole [offset, length) span must lie
|
|
@@ -27,7 +27,11 @@ module Rubycc
|
|
|
27
27
|
new(text, path: path).parse
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
+
# Bytes in (lib/rubycc.rb): the .pc grammar is spelled in ASCII, and the
|
|
31
|
+
# field values are carried through (a US-ASCII-tagged Description makes
|
|
32
|
+
# String#strip raise).
|
|
30
33
|
def initialize(text, path: nil)
|
|
34
|
+
text = text.b unless text.encoding == Encoding::BINARY
|
|
31
35
|
@text = text
|
|
32
36
|
@path = path
|
|
33
37
|
@variables = {}
|
|
@@ -34,7 +34,9 @@ module Rubycc
|
|
|
34
34
|
path = SearchPath.find(name, directories: @directories)
|
|
35
35
|
raise NotFoundError, "package '#{name}' not found (searched #{@directories.join(File::PATH_SEPARATOR)})" unless path
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
# Bytes: a .pc file's Name/Description routinely carry non-ASCII (a
|
|
38
|
+
# copyright sign, an accented maintainer name).
|
|
39
|
+
@cache[name] = Parser.parse(File.binread(path), path: path)
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
# Own Cflags, then each public Requires recursively, then each
|
|
@@ -108,9 +108,15 @@ module Rubycc
|
|
|
108
108
|
# An explicit value replaces the default outright, even when it is empty
|
|
109
109
|
# (`PKG_CONFIG_SYSTEM_INCLUDE_PATH=` then means "no directory is a
|
|
110
110
|
# system directory"); only an unset variable falls back to +defaults+.
|
|
111
|
+
#
|
|
112
|
+
# The list is held as bytes (lib/rubycc.rb): +value+ comes from the
|
|
113
|
+
# environment, while the directories it is compared against in
|
|
114
|
+
# #reject_system come out of a .pc file, and for a non-ASCII directory a
|
|
115
|
+
# differing encoding never compares equal — the `-I`/`-L` the environment
|
|
116
|
+
# asked to suppress would be emitted instead.
|
|
111
117
|
def directory_list(value, defaults)
|
|
112
|
-
list = value.nil? ? defaults : value.split(File::PATH_SEPARATOR).reject(&:empty?)
|
|
113
|
-
list.map { |dir| normalize(dir) }
|
|
118
|
+
list = value.nil? ? defaults : value.b.split(File::PATH_SEPARATOR).reject(&:empty?)
|
|
119
|
+
list.map { |dir| normalize(dir.b) }
|
|
114
120
|
end
|
|
115
121
|
|
|
116
122
|
def set?(value)
|