Module: Rubycc::Type

Defined in:
lib/rubycc/type.rb

Overview

The type system for this C subset: the standard integer types (char and its signed/unsigned variants, short, int, long and _Bool), the incomplete void type, pointers to another type, one-dimensional arrays of another type, and structures (Type::StructType). The integer, pointer and array types compare by value, so any two int * are equal, and each renders itself the way a C declarator would ("int", "unsigned long", "char *", "int [10]") for use in diagnostics. Structures instead compare by identity (see Type::StructType): a struct type is the same type only when it is the very same tag definition, which is what lets a self-referential struct ("struct node { struct node *next; }") describe itself without a value-equality walk looping forever.

Every type but void and an incomplete struct reports its storage width in bytes via #size (char/_Bool 1, short 2, int 4, long 8, any pointer 8, an array its element width times its length, a struct its laid-out size) and its required boundary via #alignment (an integer type is aligned to its own width, any pointer 8, an array its element's alignment, a struct its widest member's). void has no size or alignment (see Type::VoidType) since it is only ever valid as a function's return type or as the target of a pointer; an incomplete struct likewise has neither until it is completed.

#integer? groups the standard integer types that mix freely in expressions and convert to one another implicitly; #float? (FloatType) names the two floating types, and #arithmetic? is the union of the two — every integer or floating type — the notion "arithmetic" the usual arithmetic conversions act on. #signed? / #unsigned? report an integer type's signedness — the axis that decides signed vs unsigned division, right shift and comparison — and #bool? names _Bool specifically (whose only values are 0 and 1). #int? and #char? still name those two specific types (never their unsigned cousins), #void? names void and #struct? names a structure or a union (both are aggregates that share the same lvalue/copy machinery; #union? tells the two apart).

Defined Under Namespace

Classes: Array, EnumType, FloatType, FunctionType, IntegerType, Member, Pointer, StructType, VoidType

Constant Summary collapse

Char =

The shared integer-type instances, one per distinct C type. LP64 governs the widths: int is 4 bytes, long and any pointer 8. long long normalizes to long (same width under LP64) at the point it is parsed, so no separate instance is needed here.

The character types are three distinct types (6.2.5p15): signed char and unsigned char have a fixed signedness, while plain char behaves as one or the other, and which one is implementation-defined — pinned per ABI, so it is a property of the target rather than of this subset: the x86-64 System V psABI makes plain char signed, AAPCS64 makes it unsigned. Both plain-char instances therefore exist side by side and spell themselves "char" (so #char?, #to_s and every diagnostic read alike whichever is in play); Type.plain_char picks the one a target uses, and the front end carries that choice from Compiler#compile down to every place a char type is built. Type::Char stays the signed one, which is what a caller with no target in hand (the default x86-64) means by "char".

IntegerType.new("char", 1, true)
UnsignedChar =
IntegerType.new("char", 1, false)
SChar =
IntegerType.new("signed char", 1, true)
UChar =
IntegerType.new("unsigned char", 1, false)
Short =
IntegerType.new("short", 2, true)
UShort =
IntegerType.new("unsigned short", 2, false)
Int =
IntegerType.new("int", 4, true)
UInt =
IntegerType.new("unsigned int", 4, false)
Long =
IntegerType.new("long", 8, true)
ULong =
IntegerType.new("unsigned long", 8, false)
Bool =

_Bool is treated as an unsigned 1-byte type whose stored value is only ever 0 or 1; a conversion to it lowers to "value != 0" (see the generator).

IntegerType.new("_Bool", 1, false, bool: true)
Int128 =

The GNU 128-bit integer types (__int128 and unsigned __int128), 16 bytes wide and 16-byte aligned. They are ordinary IntegerTypes for classification (#integer?, #signed?, sizeof/_Alignof, the usual arithmetic conversions), but their value does not fit a single 64-bit slot: the generator represents a 128-bit value the way it represents a small struct — as a 16-byte stack object whose low eightbyte lives at +0 and high at +8, its address carried in an ordinary vreg — and lowers each supported operation to 64-bit ops on the two halves. The width alone (size 16) marks them apart from every other integer type; the generator's #wide128? tests it.

IntegerType.new("__int128", 16, true)
UInt128 =
IntegerType.new("unsigned __int128", 16, false)
Float =

The shared floating-type instances (Type::Float, Type::Double).

FloatType.new("float", 4)
Double =
FloatType.new("double", 8)
Void =

The lone void. Referred to everywhere as Type::Void.

VoidType.new
VaListTag =

The System V AMD64 psABI representation of a va_list element: the __va_list_tag structure a call's variable arguments are read through. Its layout — a 32-bit gp_offset (the byte offset of the next integer argument still in the register-save area), a 32-bit fp_offset (the same for a vector argument), an overflow_arg_area pointer (the next argument that spilled onto the stack) and a reg_save_area pointer (the base of the saved argument registers) — is fixed by the ABI, so building it here from the ordinary #define layout path (size 24, 8-byte aligned) matches what a System V compiler and its C library agree on. A single shared instance stands for the tag, and, being a StructType, it compares by identity, so the generator recognizes "pointer to __va_list_tag" by object identity when type-checking a va_start/va_arg/va_end operand.

StructType.new("__va_list_tag").tap do |tag|
  tag.define([
               ["gp_offset", UInt],
               ["fp_offset", UInt],
               ["overflow_arg_area", Pointer.new(Void)],
               ["reg_save_area", Pointer.new(Void)]
             ])
end
BuiltinVaList =

The type the built-in __builtin_va_list typedef names: a one-element array of __va_list_tag. The array shape is what makes a va_list object decay to a __va_list_tag * in every expression context (so passing one to a helper hands over a pointer to the same object, and va_start/va_arg write through it), while a local declaration still reserves the whole 24-byte tag as a stack object — exactly the System V convention.

Array.new(VaListTag, 1)
AArch64VaListTag =

The AAPCS64 representation of a va_list element. AArch64 splits the two register files System V folds into one save area, so the tag has five fields rather than four (AAPCS64 §B.4 / the Arm-64 va_list): __stack the next stack argument, __gr_top and __vr_top the ends of the integer and vector save areas, and __gr_offs / __vr_offs signed byte offsets from those tops. The offsets run the other way from System V's: they start negative (the whole file still to be read) and climb toward zero, at which point the file is spent and the argument comes off __stack. The layout (size 32, 8-byte aligned) is what the AArch64 C library agrees on, and the tag compares by identity, so the generator recognizes it exactly as it does the System V one.

StructType.new("__va_list").tap do |tag|
  tag.define([
               ["__stack", Pointer.new(Void)],
               ["__gr_top", Pointer.new(Void)],
               ["__vr_top", Pointer.new(Void)],
               ["__gr_offs", Int],
               ["__vr_offs", Int]
             ])
end
AArch64BuiltinVaList =

The AArch64 counterpart of BuiltinVaList: a one-element array of the five-field tag. Making it an array (rather than the bare struct gcc's __builtin_va_list happens to be) has the same decay-to-pointer effect the System V form relies on, and it is ABI-identical at a call boundary: a 32-byte va_list is passed by reference under AAPCS64 6.4.2 (a pointer to the object in a single integer register), which is exactly what the decayed array pointer already is. Forwarding a va_list to vprintf therefore lands the same pointer in the same register a gcc caller would.

Array.new(AArch64VaListTag, 1)

Class Method Summary collapse

Class Method Details

.character?(type) ⇒ Boolean

True for the character types (6.2.5p15): either plain char and the two explicitly signed ones. This is what "an array of character type", the form a string literal may initialize, means; _Bool is one byte wide too but is not a character type.

Returns:

  • (Boolean)


1231
1232
1233
1234
# File 'lib/rubycc/type.rb', line 1231

def self.character?(type)
  type.equal?(Char) || type.equal?(UnsignedChar) ||
    type.equal?(SChar) || type.equal?(UChar)
end

.composite(first, second) ⇒ Object

The composite type of two declarations of the same object (6.2.7p3), or nil when the two types are not compatible — the signal a declaration merge turns into its "conflicting types" diagnostic.

One case of that paragraph matters in this subset: when one declaration is an array type of known size and the other an array type of unspecified size, the composite type is the array type of known size. So

extern int tbl[];  int tbl[3] = {1, 2, 3};

declares one object of type "int [3]" in either order, and a later sizeof/subscript measures the completed type rather than the unbounded reference. Two known but different sizes are incompatible, and so are incompatible element types. The recursion into the element type carries the same rule through a multidimensional array ("extern int m[4];" against "int m[4]"), whose inner dimensions are always known and so must agree.

Every other pair of types composes exactly when the two are identical, which is what the equality comparison at each declaration-merge site meant before this rule existed. The paragraph's remaining cases — a function type declared with a parameter type list against one declared without, and a pointer to either of the above — do not arise here: this subset merges function signatures by equality (it models no unprototyped declaration), and a parameter of array type is adjusted to a pointer by the parser.



1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
# File 'lib/rubycc/type.rb', line 1216

def self.composite(first, second)
  return first if first == second
  return nil unless first.array? && second.array?

  element = composite(first.element, second.element)
  return nil if element.nil?
  return nil if first.length && second.length && first.length != second.length

  Array.new(element, first.length || second.length)
end

.plain_char(signed) ⇒ Object

The plain-char instance a target uses: the signed one when its ABI makes plain char signed (x86-64 System V), the unsigned one otherwise (AAPCS64). Neither is signed char/unsigned char, which keep their own fixed-signedness instances whatever the target.



1188
1189
1190
# File 'lib/rubycc/type.rb', line 1188

def self.plain_char(signed)
  signed ? Char : UnsignedChar
end