Skip to content

v0.2.8

Pre-release
Pre-release

Choose a tag to compare

@tamnd tamnd released this 01 Sep 03:52
· 17 commits to main since this release
v0.2.8
f70ddef

Added

  • Statement checking in rucc-sema, and with it the function definition and the walk over a whole translation unit. This is the one walk in the checker that carries state, because a statement is legal or not depending on what encloses it: whether break is allowed is a question about the loops and switches around it, what return may carry is a question about the function it is in, and a goto may name a label fifty lines further down. Labels are therefore resolved over the whole function rather than in order, a label is created where its name is first met whether that is the jump or the definition, and what is left undefined at the end of the function is reported then, sorted by where it was written so that two runs of the same input report in the same order. GNU's __label__ is implemented rather than refused, since a macro that jumps to its own end has to be expandable twice in one function and the kernel is full of them, so a block-local label shadows the function-wide one and is undone when the block ends. A switch collects its cases into one table and patches each case statement with its entry once the table exists, which is what a jump table wants to read and what keeps a nested switch from interleaving its cases with the ones outside it. A case value is measured against the type that was written and not the promoted one, so case 300 on a char is worth saying even though 300 is a perfectly good int. A function body is one scope holding its parameters and not two, which is why void f(int a) { int a; } is a redeclaration and void f(int a) { { int a; } } is not, and the parameters are declared once by the type builder and bound again by the definition so that the prototype's n and the body's n are one declaration. An expression statement holds the value of its expression rather than a conversion of it to void, which is what lets GNU's statement expression take the type of its last statement. What is not here is reachability, since control reaches end of non-void function is a question about a control flow graph and the answer to it is in the IR.

  • The expressions that name a type in rucc-sema, which is the cast, sizeof, alignof, offsetof, _Generic, va_arg and the two __builtin forms that take a type name. Each of them was reported as unsupported until there was a type builder to ask, and each of them is now checked and folded where the language says it is a constant. They live apart from the other operators because they behave differently: an operator that names a type asks the type builder a question before it looks at any value, and most of them leave a number in the tree with the operand gone, which is not an optimization but what the language says they are, since int a[sizeof(int)]; is a fixed array and not one whose bound has to be worked out later. The details worth calling out are the ones where the obvious implementation is wrong. A cast to void becomes the node the tree already has for a value being discarded rather than a second kind of node meaning the same thing. sizeof does not apply the value conversion, which is the whole reason sizeof a on an int[4] is sixteen and not the size of the pointer it would have become anywhere else, and it is also why the message about a function type is a message about a function type rather than an answer about a pointer. sizeof of a variable length array is the array's own size expression rather than a fresh one built from the bound, because C evaluates that expression once where the array was declared and a compiler that emits it again at each sizeof calls whatever the bound calls a second time. An array's alignment is its element's however deep the array goes, which is an answer even where its size is not. A type with no size is measured as one with a warning, since that is what GNU C does so that p + 1 on a void * means what everyone who writes it means, and a type with no definition is an error that names the type. Every message about the alignment says __alignof__ whatever the program wrote, which is what gcc prints and what a build log that greps for it wants. The casts that are refused are refused by which side of the cast was wrong, so an array type, a function type, a non-scalar target, an aggregate operand, a pointer where a floating type was asked for and a floating type where a pointer was, each with gcc 13.3's wording measured rather than recalled, and a cast of a record to its own type is accepted because gcc accepts it and it does nothing. The two casts that are allowed and still warn are a pointer to an integer of another width and back, which is the one gcc warns about by default because the value does not survive the round trip. _Generic checks every association whether or not it is the one selected, since a mistake in an association is a mistake wherever it was written, but it selects on the type the controlling expression has after its conversions, which is why an int[4] selects int *. __builtin_choose_expr checks only the arm it takes, which is the entire reason the operator exists rather than being written as a conditional. __builtin_types_compatible_p ignores the top level qualifiers, so a const int and an int are the same answer and an int[3] and an int[4] are not. offsetof walks a path of members and subscripts through anonymous members and refuses to take the offset of a bit-field, which has no byte to be at. va_arg is a node rather than a call, because what it becomes is the target's own sequence of loads and not a function anything links against, and it warns where the type asked for is one the default argument promotions mean was never in the list. It does not yet check that its first argument is a va_list, since the type to check against is __builtin_va_list and this compiler has no builtin declarations yet, so any pointer is accepted in the meantime.

  • Declaration checking in rucc-sema, which is what turns the checker from something that can be handed one expression into something that can be handed a name. The type builder already answered what a declarator says, and this answers everything else a declaration decides, which is four things about each name and one relation between the declarations that share it. The four are what kind of thing it is, who else can see it, how long it lives and how much of a definition it is, and not one of them is written down anywhere in the source: int x; at file scope is an external, static, tentative definition, and the same three words in a block are a local automatic one, and the only difference between them is where they are. The tentative state is kept as a state of its own rather than folded into either of the other two, because a tentative definition is a definition only if nothing else in the translation unit defines the name, which is not known at the point it is read, and a compiler that decides early gets either an error on int x; int x; or two objects. A redeclaration is merged into the declaration it repeats rather than added beside it, so the name keeps pointing at one object, the type becomes the composite of the two, which is what fills the bound of an int a[]; in from the int a[3]; below it, and the state becomes the stronger of the two. The rules about linkage are the ones every real header depends on and each has gcc 13.3's wording, measured rather than recalled: static after a plain declaration and a plain declaration after static are both errors, and extern after static is not, because extern says nothing about which linkage it wants and takes the one the name already has, which is what lets a library declare a symbol it hides. A declaration with linkage answers to any declaration of the name in sight and one without linkage answers only to its own scope, which is what makes a local variable called printf legal. What a name is allowed to be is checked too, so a name that already means a type or an enumerator is a different kind of symbol, an incomplete type has no storage size, a void object is worded one way in a block and another at file scope because that is what gcc prints, and a variable length array may be automatic and may not be static. typedef is here rather than in the type builder, since deciding that a declaration declares a type rather than an object is a declaration's decision, and the same name may be typedefed twice for the same type, which is what lets two headers that both define size_t be included by one file. alignas is folded and checked here, so a value that is not a power of two is refused, one that would weaken the type is refused, alignas(0) is accepted and ignored as C23 says, and asking for an alignment on a typedef or on a function is refused. static_assert is folded and reports its message quoted the way it was written. A scalar initializer goes through the same conversion an assignment does, with the wordings gcc uses for an initializer rather than the ones it uses for an assignment, since a person reading initialization of 'char *' from incompatible pointer type 'int *' is being told which of the two they wrote. Two checks are deliberately left out rather than approximated. A file-scope initializer is not required to be constant, because the folding has no address constants yet and int *p = &x; is the ordinary case rather than the exotic one, so the check would be wrong far more often than it would be right, and the constexpr case, which is arithmetic and which the folding does answer, is checked. And the warning that gives a tentative int a[]; one element is not here, because gcc decides that at the end of the translation unit and a declaration in the middle of a file has no way to know what comes after it. A function definition waits on statements and a braced initializer waits on initialization, and both report themselves as not supported yet in the meantime.

Changed

  • The reference compiler for the measured diagnostics is now gcc 16 rather than gcc 13, and several diagnostics that were warnings are errors as a result. gcc 14 promoted -Wimplicit-int, -Wint-conversion, -Wincompatible-pointer-types and -Wreturn-mismatch from warnings to errors, and gcc 15 and 16 kept them there. Those four had been warnings for thirty years, and the code that relied on them is exactly the code that breaks when a pointer is wider than an int, which is why every compiler agreed to stop accepting it. So an incompatible pointer in an assignment, an argument, an initializer or a return is an error here, an integer meeting a pointer without a cast is an error, a declaration whose type defaults to int is an error, and a return with no value in a function returning non-void or with a value in a function returning void is an error. Discarding a const is still a warning, because losing a qualifier breaks a promise the code made to itself rather than confusing the hardware about what a value is. A static, an extern or a typedef in the first clause of a for loop is no longer reported by default, since gcc only mentions it under -pedantic and enough code declares a counter that way that following the letter of the rule would reject programs everyone else builds.

What's Changed

  • sema: check the expressions that name a type by @tamnd in #81
  • sema: check declarations by @tamnd in #82
  • sema: check statements and function definitions by @tamnd in #83
  • release: 0.2.8 by @tamnd in #85

Full Changelog: v0.2.7...v0.2.8