C "clockwise/spiral" rule to understand declarations
c-faq.com
c-faq.com
+----------------------------+
| +-----------------------+ |
| | +------------------+ | |
| | | +-------------+ | | |
| | | | +--------+ | | | |
| | | | | +--+ | | | | |
| | | | | ^ | | | | | |
int * * ¦ ¦ ¦ VAR[1][2][3] | | |
^ | | | | | | | | | | |
| | | | | +-----+ | | | | |
| | | | +----------+ | | | |
| | | +---------------+ | | |
| | ---------------------+ | |
| +-------------------------+ |
+-------------------------------+
The type of VAR is a [1-element] array of [2-element] array of [3-element] array of pointer to pointer to ints. I drew a spiral that passes through each specifier in the correct order. To make the spiral correct it has to skip the pointer specifiers in the first three loops. This is marked by ¦.The Right-Left Rule is quoted less frequently on HN but it's a correct algorithm for deciphering C types: http://cseweb.ucsd.edu/~ricko/rt_lt.rule.html
The spiral rule can be modified to process all array specifiers before all pointer specifiers, but then you'd have to specify that the order to do so is right and then left. At that point it's just the Right-Left Rule.
+----------------------------+
| +-----------------------+ |
| | +------------------+ | |
| | | +-------------+ | | |
| | | | +--------+ | | | |
| | | | | +--+ | | | | |
| | | | | ^ | | | | | |
int ¦ ¦ * * ¦ VAR ¦ [1] ¦ [2][3] ¦
^ | | | | | | | | | | |
| | | | | +-----+ | | | | |
| | | | +----------+ | | | |
| | | +---------------+ | | |
| | ---------------------+ | |
| +-------------------------+ |
+-------------------------------+() and [] are right-associative unary type-operators, with high fixity, (or equivalently, binary type-operators when () or [] have arguments)
and * is a binary type-operator, with low fixity (i.e. lower than () or [] ), where the 'left' argument is effectively the context that's left after removing the asterisk and the 'right' argument (rather than what's to the left of the asterisk in a purely 'anatomical 'sense')
(whereas, at the expression level, * is a unary value-operator instead, while () and [] behave the same as in their type-form, except acting as value-operators instead)
int ** arr arr arr VAR[1][2][3];
By introducing these virtual storage kind specifiers, the spiral model works! It becomes apparent that these specifiers are actually real, cause the model wouldn't be consistent otherwise, and there's so much evidence for it all over the C codebases. We just tend to see the "real" part of it (sigh, stuck forever with that legacy terminology), while this is what actually happens: long double _Complex ** arr arr arr VAR[1][-1][sqrtl(-1)];
which is naturally homomorphic to most C compilations.(spoiler for those who need it: jk)
IMO the Go syntax is a vast improvement as it's much simpler and avoids the clockwise/spiral issue: https://appliedgo.com/blog/go-declaration-syntax
You can't really encode a tree structure with only linear semantics (without at least something like s-expressions).
The advantage of type after name is that it keeps the traversing order consistently pre-order (node, left, right), instead of either:
- the notoriously ridiculous spiral cdecl: reading stuff right (output), node (func name) and left (input)
- create a new name to describe the function in pre-order: Func<Input, Output>
At that point I find a uniform system (without special-casing arrays and pointers - they are also just types) would be simpler.
or, declare the type separately, like in fortran or haskell (or in fact, pre-C99 c)
I've seen julia types unnecessarily fill a whole terminal for otherwise simple operations before, and I have to say I wasn't too impressed...
(I also have no doubt that just like Go fixed C's type declarations, in another 20-30 years, Go's successor will finally fix error handling.)
It was only about 200 lines of code, and yet never have I been happier to finish a solution and never having to think about it again.
The _external_ way of doing introspection (a parser like yours) is generally very limiting in real life, as you will have to interpret all the preprocessor directives that your code follows.
e.g.:
struct Foo {
#ifdef ...
int a;
#else
float b;
#endif
}
Not only will you have to feed your parser the exact same inputs as your build system, but also some directives are built-in the compiler and may be hard to replicate.The easiest way to do introspection is _intrusive_, even though it pollute the code a bit.
e.g.
DECL_STRUCT(Foo)
{
DECL_ATTR(int, a);
}
END_DECL_STRUCT
A long time ago, Qt had a sort of hybrid intrusive parser like this (MOC?), that was 20y ago though things may have changed.Regarding external and intrusive, I used to do it in the intrusive way but found it too limiting. Here, I not only generate code but can also (potentially) add whole new extensions to the language. This was the reason I wrote a new parser instead of just using libclang's JSON AST dump. Well, that and the fact that libclang is a multi-MB dependency while my parser is ~3000 lines of C code.
Perhaps you should've started learning C with K&R. ;-)
char *(*(**foo[][8])())[]If you have to, you can make a syntax tree work, too, but you'll have to thread through the declarator parser a double (triple?) pointer for the hole in the tree where the next part goes, and at the end plug it with the specifiers-and-qualifiers part you parsed before that. Or at least that's what I had working before I gave up and switched to a prefix code. It'd probably pay to check the LCC book (Fraser & Hanson, A Retargetable C Compiler: Design and Implementation) to see what they do there, because their type representation is in fact a hash-consed tree. (The LuaJIT FFI parser needs some sort of magic fix-up pass at the end that I didn't much like, but I don't remember the specifics.)
Yeah, it required too much backtracking and state snap-shotting and resets, and I couldn't figure out a decent way of reporting good errors.
Thanks for the references. The code I have now is pretty elegant and functional, so I'm not in the mood of diving back into it. But if I ever need to change it, I'll take a look.
declaration
int
function call
*
[]
p
5
char
and you need to turn that inside out to get something like array
pointer to
function
int
(char)
This way you have a simple expression parser combined with a simple recursive tree transformation function. (The tree transformation took about 50 LOC when I implemented it for a toy parser.)A very small yet readable book, written by the original authors of C, that very clearly covers the language and even presents a C program to parse the declarations.
I would even say, that if C allows for a clear description of the procedure, clearer as plain text, it indicates how good and expressive C is…
It's not like there is an array<int> or ptr<int> syntax that can replace int[] or int*.
That said, that makes me think of perl5. I though the perl5 coders were doing some kind of sick competition: who is going to use the most implicit code, namely to read the code you would need a complete/perfect/permanent understanding of the full perl5 syntax parser to understand what some code is actually doing. I hate perl5 for that, but ultra complex syntax computer language like c++ and similar (java, rust, etc), are worse. In advanced OOP, you have no idea of what's going on if you don't embrace the full OOP model of a complex program, not to mention it does exponentially increase the syntax complexity, which is a liability in order to get a sane spectrum of alternative "real-life" compilers since those become insane to implement correctly.
If implicit there is in a computer language, it must be very little and very simple, but should be avoided as much as possible. Does it mean more verbose code, well, most of the time yes, and this is usually much better on the long run. For instance in C, I try to avoid complex expression (often I fail, because I am too used to some operators like '++' '--'), many operators should not be around, not pertinent enough (like a ? b : c) only increasing compiler complexity.
int p[5] means the type of p[5] is int (but you still have to remember valid elements are 0-4).
void (signal(void()(int))(int) means the type of (signal(something that is a void()(int))(42) is void. And void(p)(int) means the type of (p)(42) is void.
If you can remember the precedence of these operators, you automatically remember the precedence of their "type operators" as well.
If you have int on the left and some kind of declaration for foo on the right, that means that when you use foo with all of the present syntax, you get an int.
For example, in
char *(*fp)( int, float *)
if you use fp like this *(*fp)( int, float *)
you get a char.int long long unsigned number_of_days; - read it right to left, an unsigned long long int
float fraction; - read it right to left, "" reads as "pointer to", so pointer to float
My "float *" was somehow changed to "float fraction".
Ah, the asterix became italics.
long (*f)(int);
means, if you dereference f (i.e. write *f), you get long(int). If you then call it with an int, you get long.It's pattern matching against the usage.
int* c is perfectly valid syntax
and so is int * c.
The latter is my preferred convention: it helps me think of the asterisk as an operator, which, when acting at the "declaration" level, is binary in nature.
Whereas when the asterisk is used at the 'expression' level, it is unary in nature, and can be thought of effectively as a completely different operator that happens to share the same name/symbol as the declaration-based one.
int* a, b;
This looks as though it is equivalent to
int* a;
int* b;
but actually means
int* a;
int b;
Most people trying it your way hit this issue fairly quickly and revert to "int *a". It's not just a meaningless convention; it's a convention which reflects the grammar of the language just as conventions on indentation reflect it.
"int * a" is not as bad, in that it is ambiguous rather than misleading, but I would avoid it for similar reasons.*
Besides, a similar argument could be made of simultaneous array declarations, or things like int *f(), (*g)(); in the latter example, if I were lazy enough to use such a simultaneous declaration, at least I would still prefer to use proper grouping and spacing: int (* f()), (* g)();
Or, rely on the knowledge that the comma type-operator in this context binds less strongly than the asterisk type-operator, but more strongly than the type primitive, which causes it to be distributed to all the operands of the comma operator. Which is probably better than relying on proximity-by-convention heuristics in the first place.
Yes, some people recommend one declaration per line. It's generally a defensive habit, needed if you use spacing which doesn't reflect the grammar of the language. It's the C programming equivalent of double-knotting shoe-laces to stop them coming undone rather than recognising that the bow has been tied with a granny knot rather than a reef knot.
I do concede that if I had multiple complex declarations like int *f(), (*g)(); I would separate them on to different lines, but there is no need for that with easily read declarations such as int *a, b;.
So effectively I like to think of all symbols in a declaration statement as "operators" in some broader sense. As you say, you could think of this as "returning a type", semantically speaking, though I think more usefully I would talk about the operator as having the particular operational side-effect of allocating the correct kind of memory for that object, as per the declared type. Of course I realise that the language doesn't formally define these symbols as 'operators acting in a declaration context' as such in the standard; but, equally, the actual wording doesn't prevent you from viewing these symbols in such an operational manner either. And I find thinking of them as operators in this manner to be more informative and predictive than the super-reductive "it's just what the syntax is", which could be applied to any part of the language, reducing all discussion about semantics to nothing.
And "comma type-operator" because, well, inevitably if I just call it "comma operator", even if I'm explicitly pointing out the fact that I'm talking about the declaration rather than the expression context, someone will miss the point anyway and fixate on the fact that "this is not the comma operator" (which would be technically correct but totally not the point); therefore I take some creative liberties and refer to it as a "type-operator", in that it only carries these semantics in a declaration context :)
Regarding the granny knot analogy, I agree, but at the same time you could argue that complexity is a matter of perspective, and therefore one could argue similarly that you might as well disambiguate int * a by treating it as an operator (semantically speaking) and thus spacing it properly, and putting each declaration on a single line, as opposed to oversimplifying syntax by relying on arbitrary proximity conventions, in order to combining a bunch of declarations together; which would be like saying I'm hoping this knot is too simple to be a reef knot, I'm hoping nobody will think it's a reef knot, so no need for a double knot here. :p
In any case, I don't necessarily disagree with placing the asterisk close to the variable, but I consider this mostly a stylistic issue, and stylistically I prefer spacing the two. What I do object to is that this is the one correct way; I think it's prone to interpretation problems as much as the others (the specific issue you pointed out notwithstanding), and one should prefer proper grouping for disambiguation, rather than relying on proximity conventions. So to the extent that spacing things encourages one to provide clearer groupings and keep things in separate lines, leading to more robust code, I would prefer spacing for that reason alone.
(unless my team's style guide forced me to, of course. I'm not going on a holy war over this, it's just my preferred style/philosophy :D )
(sorry for the long reply, I'm bored in a cafe and procrastinating from actual work. thanks for the discussion and food for thought!)
A lot of people start with the idea that the syntax of C declarations is '<type> <variable_name>'. This works fine for simple cases, but it's completely wrong. What a declaration like 'int x' actually means is the following:
declare a variable x of a type such that the expression x is of type int
In such a simple case this seems unnecessarily long-winded, but now let's look at a more complex case: int (*p[4]) (int x, int y);
declare a variable p of a type such that (*p[i])(x,y) is of type int
If dereferencing the ith element of p and then calling the result with two arguments gives us an int, then p must be an array of pointers to functions that take two arguments and return int. If you saw the expression '(*p[i])(x,y)' in some code, you'd have no difficulty figuring out that p must be an array of function pointers. So you needn't have any difficulty when reading the declaration either.One slightly confusing thing here is that the nesting of the expression syntax is the opposite of the nesting of the type. The expression is
funcall(deref(array_index(p, i)), [x, y])
whereas the type is array_of(pointer_to(function(args: [int, int], returning: int)))
This makes sense once you understand that the expression is to be interpreted just as a normal expression. The first thing you do with an array of something is index into it. So indexation is going to be the most deeply nested part of the expression, even though the outermost layer of the type is 'array of ___'.One additional source of confusion is the '*' operator and the need for additional parentheses in function pointer declarations. In C, function pointers dereference to themselves, so 'p()' and '(*p)()' are equivalent if p is a function pointer. However, in a type declaration you need something to distinguish a function pointer from a function, so the '*' has to be present. Why can't we just write 'int *p[4](int x, int y)' in the example above? Because of how the operator precedence rules work. That expression is equivalent to '*(p[4](x,y))', so it would declare an array of functions returning pointers to integers. (You can't declare an array of functions in C, so that's invalid.)
Ad-hoc rules for interpreting C declarations miss the genius of their underlying concept. You already know all the syntax you need to understand a C type declaration! It's just C expression syntax.
- & &mut doesn't make sense: and a variable of type & &mut can't mutate the underlying object, it's practically equivalent to a & &, even worse, since the inner is &mut, you can't have 2 & &mut that point to the same &mut (mut xor shared).
- &mut &: mutable reference that point to a shared reference, means you can change the outer to point to another shared reference, but you can't change content of the inner, for example:
let a: &str = "hello";
let b: &str = "world";
let mut c = &a;
let d = &mut c;
*d = &b;
// (*d).make_ascii_lowercase(); // not allowed
- &mut &mut: similar to the above, but you can also change the content of the inner.Sadly the link to Linus Torvalds' explanation of why it doesn't work has died, but the Archive remembers:
https://web.archive.org/web/20141218085356/https://plus.goog...
> Heh, no, typedefs don't help anything, there's a reason we don't use them in the Linux kernel whenever possible.
This surprises me, can someone explain that remark and elaborate on the reason it alludes to?
Otherwise, if you actually use the variable directly, you are requiring the user to remember the underlying type of the typedef.
Larger program, hundred plus typedefs, unbearable burden on reader.