core/sprinter.rb
module Rubinius
class Sprinter
def self.cache
@cache
end
def self.get(format)
if ec = @cache
if sprinter = ec.retrieve(format)
return sprinter
end
end
sprinter = new(format)
ec.set(format, sprinter) if ec
sprinter
end
def initialize(format)
begin
Rubinius::Type.object_singleton_class(self).dynamic_method :call do |g|
Builder.new(self, format, g).build
end
rescue Rubinius::ToolSets::Runtime::CompileError => e
raise RuntimeError, "failed to compile printf format: #{format}", e
end
end
private :initialize
def debug_print
printer = Rubinius::ToolSets::Runtime::Compiler::MethodPrinter.new
printer.input(method(:call).executable)
printer.bytecode = true
printer.run
end
def zero_two_expand_integer(int)
int = Integer(int) unless int.kind_of? Fixnum
return int.to_s if int < 0 or int >= 10
"0#{int}"
end
def zero_expand_integer(int, width)
int = Integer(int) unless int.kind_of? Fixnum
str = int.to_s
return str if width <= str.size
diff = width - str.size
if int < 0
case diff
when 1
"-0#{-int}"
when 2
"-00#{-int}"
when 3
"-000#{-int}"
else
"-#{'0' * diff}#{-int}"
end
else
case diff
when 1
"0#{str}"
when 2
"00#{str}"
when 3
"000#{str}"
when 4
"0000#{str}"
else
("0" * diff) << str
end
end
end
def zero_expand_leader(int, width)
if int < 0
zero_expand_integer(int, width)
else
zero_expand_integer(int, width-1)
end
end
def space_expand_integer(int, width)
int = Integer(int) unless int.kind_of? Fixnum
str = int.to_s
width = -width if width < 0
return str if width <= str.size
diff = width - str.size
case diff
when 1
" #{str}"
when 2
" #{str}"
when 3
" #{str}"
when 4
" #{str}"
else
(" " * diff) << str
end
end
def space_expand_leader(int, width)
if int < 0
space_expand_integer(int, width)
else
space_expand_integer(int, width-1)
end
end
def space_expand_integer_left(int, width)
int = Integer(int) unless int.kind_of? Fixnum
str = int.to_s
return str if width <= str.size
diff = width - str.size
case diff
when 1
"#{str} "
when 2
"#{str} "
when 3
"#{str} "
when 4
"#{str} "
else
str + (" " * diff)
end
end
def space_expand_leader_left(int, width)
if int < 0
space_expand_integer_left(int, width)
else
space_expand_integer_left(int, width-1)
end
end
def as_int(int)
if int.kind_of? Integer
return int
else
Integer(int)
end
end
def compute_space(int)
if int >= 0
" "
else
""
end
end
def compute_plus(int)
if int >= 0
"+"
else
""
end
end
def digit_expand_precision(int, precision)
str = int.to_s
diff = Integer(precision) - str.size
if int < 0
# account for the - sign infront
diff += 1
return str if diff <= 0
case diff
when 1
"-0#{-int}"
when 2
"-00#{-int}"
when 3
"-000#{-int}"
else
"-#{'0' * diff}#{-int}"
end
else
return str if diff <= 0
case diff
when 1
"0#{str}"
when 2
"00#{str}"
when 3
"000#{str}"
when 4
"0000#{str}"
else
("0" * diff) << str
end
end
end
def space_expand_left(str, width)
width = Integer(width)
sz = str.size
width = -width if width < 0
return str if sz >= width
diff = width - sz
case diff
when 1
"#{str} "
when 2
"#{str} "
when 3
"#{str} "
when 4
"#{str} "
else
str + (" " * diff)
end
end
def space_expand(str, width)
width = Integer(width)
return space_expand_left(str, -width) if width < 0
sz = str.size
return str if sz >= width
diff = width - sz
case diff
when 1
" #{str}"
when 2
" #{str}"
when 3
" #{str}"
when 4
" #{str}"
else
(" " * diff) << str
end
end
class Builder
def initialize(code, format, g)
@code, @format = code, format
@append_parts = 0
@g = g
end
private :initialize
def build
self.parse
@g.required_args = @arg_count
@g.total_args = @arg_count + 1
# We won't use it, but we accept a splat; our semantics require
# that we ignore any excess arguments provided.
@g.splat_index = @arg_count
@g.local_count = @arg_count + 1
if @index_mode == :absolute
@g.local_names = (0...@arg_count).map { |i| :"#{i + 1}$" } + [:splat]
else
@g.local_names = (0...@arg_count).map { |i| :"arg#{i}" } + [:splat]
end
@g.string_build @append_parts
@g.ret
end
def invert
@g.push_int 0
@g.swap
@g.send :-, 1
end
def is_negative
@g.push_int 0
@g.send :<, 1
end
def justify(direction, may_be_negative=true)
if may_be_negative && direction != :ljust
width_done = @g.new_label
@g.dup
is_negative
if_false do
@g.send direction, 1
@g.goto width_done
end
invert
@g.send :ljust, 1
width_done.set!
else
@g.send direction, 1
end
end
def next_index(specified=nil)
if specified
specified = specified.to_i
raise ArgumentError, "invalid positional index" if specified == 0
if @index_mode == :relative
raise ArgumentError, "unnumbered mixed with numbered"
end
@index_mode = :absolute
@arg_count = specified if specified > @arg_count
specified - 1
else
if @index_mode == :absolute
raise ArgumentError, "unnumbered mixed with numbered"
end
@index_mode = :relative
(@arg_count += 1) - 1
end
end
def append_literal(str)
@g.push_literal str
append_str
end
def append_str
@append_parts += 1
end
def encode_value(str)
str.force_encoding(@format.encoding)
str
end
def is_zero
@g.push_int 0
@g.send :equal?, 1
end
class Atom
def initialize(b, g, format_code, flags, name = nil)
@b, @g = b, g
@format_code, @flags = format_code, flags
@name = name
@f_alt = flags.index(?#)
@f_zero = flags.index(?0)
@f_plus = flags.index(?+)
@f_ljust = flags.index(?-)
@f_space = flags.index(?\ )
@just_dir = @f_ljust ? :ljust : :rjust
@prefix = PREFIX[@format_code] if @f_alt
@full_leader_size = @prefix ? @prefix.size : 0
@full_leader_size += 1 if @f_plus || @f_space
end
private :initialize
def prepend_prefix
@g.push_literal @prefix
@g.string_dup
@g.string_append
end
def set_value(ref)
unless @name
@field_index = @b.next_index(ref)
end
end
def set_width(full, ref, static)
@width_static = static && static.to_i
if !@name && full && !static
@width_index = @b.next_index(ref)
end
@has_width = @width_static || @width_index
end
def set_precision(full, ref, static)
@prec_static = static && static.to_i
if !@name && full && !static
@prec_index = @b.next_index(ref)
end
@has_precision = @prec_static || @prec_index
end
def push_value
if @name
@g.push_local 0
@g.push_literal @name
@g.send :fetch, 1
else
@g.push_local @field_index
end
end
def push_width_value
if @width_static
@g.push_int @width_static
elsif
@g.push_local @width_index
else
raise "No width to push"
end
end
def push_width(adjust=true)
if @width_static
raise ArgumentError, "width too big" unless @width_static.class == Fixnum
if adjust && @full_leader_size > 0
@g.push_int(@width_static - @full_leader_size)
else
@g.push_int @width_static
end
elsif @width_index
@g.push_local @width_index
@b.force_type :Fixnum, :Integer do
# If we had to do a conversion, we check we ended up
# with a Fixnum
@g.dup
@b.push_Fixnum
@g.swap
@g.kind_of
@b.if_false do
@b.raise_ArgumentError "width too big"
end
end
else
raise "push without a width"
end
end
def push_precision_value
if @prec_static
@g.push_int @prec_static
else
@g.push_local @prec_index
end
end
def push_precision
if @prec_static
raise ArgumentError, "precision too big" unless @prec_static.class == Fixnum
@g.push_int @prec_static
elsif @prec_index
@g.push_local @prec_index
@b.force_type :Fixnum, :Integer do
# If we had to do a conversion, we check we ended up
# with a Fixnum
@g.dup
@b.push_Fixnum
@g.swap
@g.kind_of
@b.if_false do
@b.raise_ArgumentError "precision too big"
end
end
else
raise "push without a precision"
end
end
def push_format_string
float_format_code = @format_code
leader = "%#{@flags}"
if !@width_index && !@prec_index
leader << @width_static.to_s if @width_static
leader << ".#{@prec_static}" if @prec_static
@g.push_literal "#{leader}#{float_format_code}"
else
format_parts = 1
if @prec_static
@g.push_literal ".#{@prec_static}#{float_format_code}"
else
@g.push_literal(float_format_code)
if @prec_index
push_precision
@g.send :to_s, 0
format_parts += 1
if @width_index
@g.push_literal "."
format_parts += 1
end
end
end
if @width_static
leader << @width_static.to_s
elsif @width_index
push_width
@g.send :to_s, 0
format_parts += 1
end
leader << "." if @prec_index && !@width_index
@g.push_literal leader
@g.string_dup
format_parts.times do
@g.string_append
end
end
end
def positive_sign
if @f_plus
'+'
elsif @f_space
' '
else
''
end
end
def justify_width(adjust=true)
if @has_width
push_width adjust
@b.justify @just_dir, @width_static.nil?
end
end
def zero_pad(pad="0")
if @has_precision
push_precision
elsif @has_width && @f_zero
push_width true
else
# exit early if no width has been pushed
return
end
# let the caller adjust the width if needed
yield if block_given?
@g.push_literal pad
@g.send :rjust, 2
end
attr_reader :width_static
def leader?
@full_leader_size > 0
end
end
class CharAtom < Atom
def bytecode
push_value
# We need to push the original value here
# because we have to possibly make multiple
# attempts to coerce it to either a string
# or an integer. If we don't duplicate we can't
# retry it for another type with the same original
# value.
@g.dup
@b.try_type :String, :to_str do
# Attempt failed, pop the nil value
# and retry for Integer
@g.pop
@g.dup
@b.force_type :Fixnum, :Integer
@g.send :chr, 0
end
@g.dup
@g.send :length, 0
@g.push_int 1
@g.send :equal?, 1
@b.if_false do
@b.raise_ArgumentError "%c requires a character"
end
justify_width
# Swap the given value with the coerced value and
# pop off the given value afterwards
@g.swap
@g.pop
@b.append_str
end
end
class IntegerAtom < Atom
def fast_common_case?
@f_zero && @width_static && !@has_precision && !@f_space && !@f_plus && !@f_ljust
end
def expand_with_width
if @f_ljust
if @f_space || @f_plus
@g.send :space_expand_leader_left, 2
else
@g.send :space_expand_integer_left, 2
end
else
if @f_zero
if @f_space || @f_plus
@g.send :zero_expand_leader, 2
else
@g.send :zero_expand_integer, 2
end
else
if @f_space || @f_plus
@g.send :space_expand_leader, 2
else
@g.send :space_expand_integer, 2
end
end
end
end
def bytecode
if fast_common_case?
@g.push_self
push_value
if @width_static == 2
@g.send :zero_two_expand_integer, 1
else
@g.push_int @width_static
@g.send :zero_expand_integer, 2
end
@b.append_str
else
@g.push_self
push_value
@g.send :as_int, 1
val_idx = @g.new_stack_local
@g.set_stack_local val_idx
@g.pop
# generic case
if @f_space
@g.push_self
@g.push_stack_local val_idx
@g.send :compute_space, 1
@b.append_str
elsif @f_plus
@g.push_self
@g.push_stack_local val_idx
@g.send :compute_plus, 1
@b.append_str
end
if @has_precision
@g.push_self
@g.push_stack_local val_idx
push_precision_value
@g.send :digit_expand_precision, 2
if @has_width
@g.push_self
@g.swap
push_width_value
if @f_ljust
@g.send :space_expand_left, 2
else
@g.send :space_expand, 2
end
end
@b.append_str
elsif @has_width
@g.push_self
@g.push_stack_local val_idx
push_width_value
expand_with_width
@b.append_str
else
@g.push_stack_local val_idx
@g.send :to_s, 0
@b.append_str
end
end
end
end
class ExtIntegerAtom < Atom
def pad_negative_int(padding)
zero_pad(padding) do
# decrease the width by 2 to account for the ".." below
@g.push_int 2
@g.send :-, 1
end
@g.push_literal ".."
@g.string_dup
@g.string_append
end
def prepend_prefix_bytecode
skip_prefix = @g.new_label
# For 'x' and 'X', don't prepend the "0x" prefix
# if the value is zero
if @format_code == 'x' || @format_code == 'X'
push_value
@b.is_zero
@g.goto_if_true skip_prefix
end
prepend_prefix
skip_prefix.set!
end
def format_negative_int(radix)
# (num + radix ** num.to_s(radix).size).to_s(radix)
@g.push_int radix
@g.dup_many 2
@g.send :to_s, 1
@g.send :size, 0
@g.send :**, 1
@g.send :+, 1
@g.push_int radix
@g.send :to_s, 1
(radix - 1).to_s(radix)
end
def bytecode
radix = RADIX[@format_code]
push_value
# Bignum is obviously also perfectly acceptable. But we
# just address the most common case by avoiding the call
# if we've been given a Fixnum. The call is enough
# overhead to bother, but not something to panic about.
@b.force_type :Fixnum, :Integer
if @f_plus || @f_space
@g.dup
# stash away whether it's negative
@b.is_negative
@g.dup
@g.move_down 2
@b.if_true do
# but treat it as positive for now
@b.invert
end
@g.push_int radix
@g.send :to_s, 1
else
have_formatted = @g.new_label
@g.dup
@b.is_negative
@b.if_false do
@g.push_int radix
@g.send :to_s, 1
@g.goto have_formatted
end
padding = format_negative_int(radix)
pad_negative_int(padding)
have_formatted.set!
end
# 'B' also returns an uppercase string, but there, the
# only alpha character is in the prefix -- and that's
# already uppercase
if @format_code == 'X'
@g.send :upcase, 0
end
zero_pad
if @prefix
prepend_prefix_bytecode
end
if @f_plus || @f_space
append_sign = @g.new_label
@g.swap
@b.if_true do
@g.push_literal '-'
@g.goto append_sign
end
@g.push_literal positive_sign
append_sign.set!
@g.string_dup
@g.string_append
end
if @has_precision || !@f_zero
justify_width false
end
@b.append_str
end
end
class StringAtom < Atom
def string_justify
# by default, f_space changes the padding introduced
# by justify_width, but for string, we want to go
# ahead and actually ignore it not have it count
# against the justification space width.
if @f_space
@full_leader_size -= 1
end
justify_width
if @has_precision
@g.push_int 0
push_precision
@g.send :[], 2
end
end
def bytecode
push_value
@b.force_type :String
string_justify
@b.append_str
end
end
class InspectAtom < StringAtom
def bytecode
push_value
@g.send :inspect, 0
string_justify
@b.append_str
end
end
class FloatAtom < Atom
def bytecode
push_value
@b.force_type :Float
format_done = @g.new_label
@g.dup
@g.send :finite?, 0
@b.if_true do
push_format_string
@g.send :to_s_formatted, 1, true
@g.goto format_done
end
formatted_non_finite = @g.new_label
@g.dup
@g.send :nan?, 0
@b.if_false do
@b.is_negative
@b.if_false do
@g.push_literal "#{positive_sign}Inf"
@g.goto formatted_non_finite
end
@g.push_literal '-Inf'
@g.goto formatted_non_finite
end
@g.pop
@g.push_literal 'NaN'
formatted_non_finite.set!
justify_width false
format_done.set!
@b.append_str
end
end
class LiteralAtom < Atom
def set_value(ref)
@value = ref
end
def bytecode
@b.append_literal(@value)
end
end
def push_Kernel
@g.push_const :Kernel
end
def push_Fixnum
@g.push_const :Fixnum
end
def push_String
@g.push_const :String
end
def push_Hash
@g.push_const :Hash
end
def raise_ArgumentError(msg)
@g.push_const :ArgumentError
@g.push_literal msg
@g.send :new, 1
@g.raise_exc
end
def force_type(klass, method=klass)
@g.dup
@g.push_const klass
@g.swap
@g.kind_of
if_false do
@g.push_self
@g.swap
@g.send method, 1, true
yield if block_given?
end
end
def try_type(klass, method)
@g.dup
@g.push_const klass
@g.swap
@g.kind_of
if_false do
@g.push_type
@g.swap
@g.push_const klass
@g.push_literal method
@g.send :check_convert_type, 3
@g.dup
if_false do
yield if block_given?
end
end
end
def if_true
l = @g.new_label
@g.goto_if_false l
yield
l.set!
end
def if_false
l = @g.new_label
@g.goto_if_true l
yield
l.set!
end
def parse
@arg_count = 0
@index_mode = nil
atoms = []
# Always push an empty string at first for correct
# encoding protocols
atom = LiteralAtom.new(self, @g, "", "")
atom.set_value(encode_value(""))
atoms << atom
pos = 0
while match = RE.match_start(@format, pos)
pos = match.full.at(1)
_,
plain_string,
whole_format,
name_format,
flags_a,
field_ref_a,
flags_b,
width_full, width_ref, width_static,
prec_full, prec_ref, prec_static,
field_ref_b,
format_code,
literal_char,
name_reference,
invalid_format = *match
flags = "#{flags_a}#{flags_b}"
if plain_string
atom = LiteralAtom.new(self, @g, format_code, flags)
atom.set_value(plain_string)
atoms << atom
elsif literal_char
atom = LiteralAtom.new(self, @g, format_code, flags)
atom.set_value(literal_char)
atoms << atom
elsif invalid_format || (field_ref_a && field_ref_b)
raise ArgumentError, "malformed format string: #{@format.inspect}"
else
field_ref = field_ref_a || field_ref_b
if name_reference
format_code = "s"
@index_mode = :name
name = name_reference[2...-1].to_sym
elsif name_format
@index_mode = :name
name = name_format[1...-1].to_sym
end
klass = AtomMap[format_code[0]]
unless klass
raise ArgumentError, "unknown format type - #{format_code}"
end
atom = klass.new(self, @g, format_code, flags, name)
atom.set_width width_full, width_ref, width_static
atom.set_precision prec_full, prec_ref, prec_static
atom.set_value field_ref
atoms << atom
end
end
if @index_mode == :name
exception = @g.new_label
continue = @g.new_label
@arg_count = 1
@g.passed_arg @arg_count
@g.goto_if_true exception
push_Hash
@g.push_local 0
@g.kind_of
@g.goto_if_false exception
@g.goto continue
exception.set!
raise_ArgumentError "named format string needs single Hash argument"
continue.set!
elsif @index_mode != :absolute
no_exception = @g.new_label
# If we've used relative arguments, and $DEBUG is true, we
# throw an exception if passed more arguments than we need.
# Check this first; it's much faster, and generally false
@g.passed_arg @arg_count
@g.goto_if_false no_exception
gva = Rubinius::ToolSets::Runtime::AST::GlobalVariableAccess
gva.new(1, :$DEBUG).bytecode(@g)
@g.goto_if_false no_exception
raise_ArgumentError "too many arguments for format string"
no_exception.set!
end
atoms.each do |atom|
atom.bytecode
end
end
end
end
end