Skip to content

v0.2.5

Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 31 Aug 23:51
· 42 commits to main since this release
v0.2.5
a782886

Added

  • The conversions in rucc-sema, which are the rules every part of the checking rests on and the one place that knows them. An expression used for its value goes through the lvalue conversion of 6.3.2.1, then the integer promotions of 6.3.1.1, then the usual arithmetic conversions of 6.3.1.8, and the order is not a convenience: an array and a function never take part in the first one, they decay instead, which is why sizeof a on an array is the array's size and not a pointer's and why the decay has to be a step of its own rather than a special case of reading. Each of them writes a node, so the tree says what happened rather than leaving the walk to the IR to work out that an int met a long somewhere, and nothing is written where nothing happens, so a dump of an expression already in the type it wanted has no noise in it. Two of them are the ones a compiler gets subtly wrong. A scalar used as a condition is a comparison against zero and not a truncation, which is why it is its own kind of conversion: (bool) 256 is true and (char) 256 is zero, and a compiler that treats those the same is wrong about one of them. And a null pointer constant is not the integer zero converted, since the constant may have any integer type and (void *)0 is one of them, so what makes it null is what it says rather than what it weighs, which means the test for it looks through the casts and the conversions rather than stopping at the first node. Reading an object drops the qualifiers and the atomicity, because neither is part of a value, so const int x; x + 1 has an int on the left of the +. A bit-field is promoted by its width rather than by the type it was declared with, so unsigned b:3 promotes to int and unsigned b:32 promotes to unsigned int, which is a separate entry point rather than a special case, because the width is a fact about the record and the tree holds only the field.

  • The scopes in rucc-sema, which is where a use of a name becomes a reference to the declaration it means. The parser already resolved names in one sense, since it had to decide which identifiers were type names, but that is a smaller question and a different one: it needed to know whether the A in (A)*b was a type and never needed to know which A, while this has to know which declaration, because that is what the use gets its type from and what the object file eventually refers to. Two of C's four namespaces are here and the other two are deliberately elsewhere, since labels are function wide rather than block scoped and a stack would only be in the way of them, and members belong to the record that declares them and are reached through a type. An ordinary name resolves to a declaration, a typedef or an enumerator, and the last two never reach the tree, because one is a name for a type the type table already holds as sugar and the other is a constant that gets folded into the expression that used it. The scoped map itself moved down into rucc-base as ScopeMap, out of the parser that had it as a private type, because semantic analysis needs the same structure with different values in it and the two crates are at the same layer rank, so neither could borrow it from the other. That is one home for the scoping and one place where the cost of it is decided, which is a map from a name to the stack of bindings for that name plus a log of what each open scope bound, so a lookup costs one hash rather than the depth of the nesting and closing a scope costs what it declared rather than a walk of everything visible from it.

  • The type category predicates in rucc-types, which is what almost every constraint in C is actually written over: an operand of % has integer type, an operand of ! has scalar type, a member of a struct has complete object type. They are in the type table rather than in semantic analysis because they are facts about types and the backend wants them too, and they are worth having in one place because each of them has a member nobody remembers. An enumeration is an integer type, so enum e x; x % 2 is legal C and a compiler that asks whether the kind is Int says it is not. _Atomic(T) is in whatever category T is in, which is the wrong way round for this question and the right way round for spelling it, so every predicate looks through the wrapper except the one asking about it. A union is not an aggregate, which is not a quirk of wording but the reason a union is initialized from its first member while an aggregate is initialized member by member. And void is an object type that is never a complete one, which are two questions rather than one, and collapsing them is how sizeof (void) ends up accepted or rejected for the wrong reason. Being modifiable is the predicate that takes a walk rather than a look, since a struct with a const member anywhere inside it cannot be assigned to, and that is the part a compiler forgets.

  • The printer for the typed tree in rucc-sema, which is what --emit=tast writes. It does not print C and does not try to, because the tree is not source any more: every conversion the language performs is a node of its own, so writing it back as C would print exactly the text that hides what there is to see. What comes out is one node per line, indented by depth, with the type spelled out at every expression, which is the artifact that answers the question an IR bug usually turns out to be, namely which conversion is missing or which one is the wrong one. A tree with jump tables in it is not a tree, since a switch holds a table of cases whose bodies are statements inside its own body and a goto names a label defined somewhere else, so those are printed as references written #n after the word that says what n counts, and the numbers are arena indices, which is what makes a dump greppable: the definition and every use of one thing carry the same number. Writing it found two gaps in the representation it prints, which is the argument for writing a printer early rather than late. A case statement had no way to reach its own value, since the values are in the switch's table and the statement held only its body, so a printer had to search the table for the statement it already had in hand and so would the walk to the IR; the statement now holds the index of its table entry, which costs four bytes it had spare. And default: existed only as a field on the switch, so nothing in the body said where it had been written, which meant a dump could not show a reader where control goes when nothing matches; it is now a statement in the body as well as the jump target on the switch, which is exactly the arrangement the cases already had.

  • The spelling of a character constant and of a string literal moved from the C printer in rucc-ast into rucc-lex, as CharConstant::spell and StringLiteral::spell, because the typed tree's printer needs the same spellings and the encoding rules they depend on were already there. Two printers with their own escapers is two printers that disagree about ?? and about where a hexadecimal escape stops running on.

  • The typed tree in rucc-sema: the arenas, a node for every typed expression and statement, the declarations with their linkage and their storage duration resolved, and the flattened initializers. It is the same shape as the untyped tree and for the same reasons, flat vectors and four byte indices and one drop at the end of the translation unit, with one difference that is the whole point of it: the type is in the node rather than in a side table beside it, because everything that walks this tree reads the type at every node, which is exactly not true of spans. Every conversion the language performs without being asked is a node of its own, so (long)a + (long)b is three nodes where the source has one operator. That is a cost paid deliberately, because the alternative is that the walk to the IR works out for itself that an int met a long somewhere, and then a second place in the compiler knows the conversion rules and is slightly wrong about them. Eight conversions are kept apart rather than left to be inferred from the two types: reading an object, an array decaying, a function decaying, one arithmetic type to another, one pointer type to another, a scalar to bool which is a comparison against zero and not a truncation, a null pointer constant which is not the same as converting the integer zero because the constant may have any integer type and (void *)0 is one, and a value being discarded. The operators are the parser's own UnaryOp and BinaryOp rather than a second set with the same names, since what the typed tree adds is not different operators, it is knowing what they are applied to. Nothing is desugared here either: a subscript is a subscript and not *(base + index), because the rewriting has exactly one home and because a diagnostic about a subscript should say subscript. What is deliberately absent is a typedef, an enumerator and a tag, since the first is a name for a type that the type table already holds as sugar and the other two are constants that have been folded into the expressions that used them, which leaves the objects and the functions, which are what has to exist at run time and what the walk to the IR wants a list of. An initializer is flattened to a list of values and the byte offsets they go at, with the contract that the object starts as zero and the entries are applied in order, so partial initialization and an overwriting designator fall out of the representation rather than needing rules of their own. An expression is twenty four bytes, a statement twenty four, a declaration thirty six, and a switch case forty eight, the last because two i128 bounds want sixteen byte alignment and nothing narrower holds a switch over __int128, and every one of those numbers has a test asserting it. The checking that fills the tree in is next.

  • spell and declare in rucc-types, which write a type back as the C declaration it is. Every diagnostic that mentions a type needs one of these and so does the typed tree's textual form, and the rule the table already follows is that a semantic decision reads the canonical type while a message reads the type as written, so a size_t prints as size_t rather than as unsigned long and a caller that wants both asks for the canonical form as well. A type is not a string of words in C, it is a declaration with a hole in it where the name goes, so this is assembled outward from the hole in the same way the parser reads a declarator inward, which is what puts int (*f[3])(char) back together and what leaves int (*)[3] with the parentheses it still needs when the hole is empty. The spellings were measured against gcc 13.3 rather than recalled, and three of them are not what a guess would give: a vector type is __vector(4) int, a tag that was never written is <anonymous>, and _Bool stays _Bool in C23 as well, where gcc prints the old spelling however the program wrote it. One is deliberately not gcc's: _Atomic(int) rather than _Atomic int, because the two spellings mean different things in front of a pointer and this crate holds _Atomic as a type rather than as a qualifier for exactly that reason.

What's Changed

  • Spell a type the way a person writes it by @tamnd in #69
  • The typed tree in rucc-sema by @tamnd in #70
  • The printer for the typed tree by @tamnd in #71
  • sema: the scopes, the conversions, and the type categories by @tamnd in #72
  • release: 0.2.5 by @tamnd in #73

Full Changelog: v0.2.4...v0.2.5