C Prog
C Prog
C Language Translation
Volume 2, Number 3
December, 1990
The Journal of C Language Translation (ISSN 1042-5721) is a quarterly publication aimed specically at implementers of C language translators such as
compilers, interpreters, preprocessors, language-to-C and C-to-language translators, static analysis tools, cross-reference tools, parser generators, lexical analyzers, syntax-directed editors, validation suites, and the like. It should also
be of interest to vendors of third-party libraries since they must interface with,
and support, vendors of such translation tools. Companies committed to C as
a strategic applications language may also be interested in subscribing to The
Journal to monitor and impact the evolution of the language and its support
environment.
c 1990, Rex Jaeschke. No portion of this
The entire contents are copyright
publication may be reproduced, stored or transmitted in any form, including
computer retrieval, without written permission from the publisher. All rights
are reserved. The contents of any article containing a by-line express the opinion
of the author and are not necessarily those of the publisher nor the authors
employer.
Editorial: Address all correspondence to 2051 Swans Neck Way, Reston, Virginia 22091 USA. Telephone (703) 860-0091. Electronic mail address via uucp
is [email protected] or [email protected].
Subscriptions: The cost for one year (four issues) is $235. For three or more
subscriptions billed to the same address and person, the discounted price is
$200. Add $15 per subscription for destinations outside USA and Canada. All
payments must be made in U.S. dollars and checks must be drawn on a U.S.
bank.
Submissions: You are invited to submit abstracts or topic ideas, however, The
Journal will not be responsible for returning unsolicited manuscripts. Please
submit all manuscripts electronically or on suitable magnetic media. Final
copy is typeset using TEX with the LaTEX macro package. Author guidelines
are available on request.
The following are trademarks of their respective companies: MS-DOS and
XENIX, Microsoft; PC-DOS, IBM; POSIX, IEEE; UNIX, UNIX System Laboratories, Inc.; TEX, American Mathmatical Society.
Contents
19. Generalizing Type Qualiers P.J. Plauger . . . . . . . . . . . . . . . . . . 165
A discussion of types, storage classes, and type qualiers, and suggestions for further generalization of type qualiers based on prior
art.
20.
21.
ii
P.J. Plauger
Abstract
The ANSI standard introduced type qualiers into the C programming
language. Signaled by the keywords const and volatile, the standard
type qualiers lie midway between types and storage classes. The common
extensions near and far also behave as type qualiers.
This article discusses why neither types nor storage classes are adequate vehicles for these added semantics. It also discusses ways to further
generalize type qualiers and use them to express a number of useful extensions to C. The concepts have been tested in a family of commercial
C compilers over the past several years.
165
166
A machine with separate I-space and D-space needed program text alone
in the code segment. To access literal data from I-space was often dicult
or impossible. So literal data and writable data shared the data segment.
Write protection of literal data went out the window.
A machine with ROM and RAM could pack program text and literal
data together in the code segment. That let the literal data be writeprotected. It also minimized the amount of RAM that had to be initialized
on program startup. Writable data was alone in the data segment.
To steer certain data objects into the read-only code segment, you put them
in a separate le. A compile-time switch specied how the contributions got
steered. It was a messy system, but it was useful to many programmers.
X3J11 wanted to do better than this. We felt that compile-time switches
were inelegant. They were also outside the charter of a language standard. We
wanted some way to bring the information into the language proper. An early
suggestion was to introduce a new storage class. Declare a data object with
storage class const and it goes into ROM instead of RAM. That meets the
immediate need, but it has a few holes.
Storage class also conveys linkage information. The rules for doing so are
already complex, because the keywords extern and static are grossly overloaded. A programmer probably wants to specify both external constant data
and internal constant data.
That argues that const should at least be a storage class qualier, not just
another overstressed storage class designator. Make it a qualier and you can
specify constant register, argument, and automatic storage as well. None of
these creatures can be placed in ROM, but you can at least help the compiler
catch obviously silly stores.
Once you decide you want to diagnose silly stores, however, you come face
to face with another shortcoming. Take the address of a data object and you
lose information about its constancy. C cant distinguish between a pointer to
static storage and a pointer to automatic storage. There is no sensible way to
require it to distinguish constant from nonconstant storage. No stores through
pointers can be checked.
A new storage class wont do the trick, nor will a storage class qualier.
That suggests that const should be part of the type information. You hardly
want to add just a single new type called const. Every data type wants to have
a constant counterpart. So it makes sense to let the keyword const qualify any
of the existing types. Naturally, a const short has the same representation as
a short. It merely carries the added proviso that it is not to be modied by
the executing program.
So far, const has much the avor of signed or unsigned. Most popular
computers today represent signed integers as twos-complement, so a change of
signedness doesnt even alter the representation. Only the values associated
with certain bit patterns change. It looks at rst blush like const can be
considered just another type part, like all the older type keywords.
167
There is one important dierence, of course. A pointer type has two opportunities to acquire constness. You can talk about a pointer to a constant
char or a constant pointer to char. In fact, you can talk about four dierent
combinations of constness. All make sense. Thats what led X3J11 to accept
the const qualier following any * in a declaration.
This calls for a decision, by the way. It is completely arbitrary how you
choose to interpret:
const char *p1;
You can say that p1 is a constant pointer to char or a pointer to constant
char. Then the alternate declaration:
char * const p2;
has the other meaning. As a matter of common sense, the committee chose to
stick with the convention in C++, from which we borrowed const. The rst
declaration above declares p1 to be a pointer to constant char. The second
declares p2 to be a constant pointer to char. (Read * const as pointer which
is constant.)
168
the various bit patterns? What operations does the language permit, on what
values? This is the realm of data types.
Yet const addresses none of these questions. If you stretch a point, you
can argue that it restricts the operations permitted by the assigning operators.
Thats not a strong case for calling const type information, however.
Storage class information traditionally deals with the addressability of data
objects. Static data is directly addressable in memory. Automatic data is on
the stack. Registers are someplace funny. A compiler that speaks assembly
language even distinguishes between static names with external and internal
linkage. The former derive by simple rules from the name itself. The latter
must be private to a given translation unit.
But once the program obtains a stored value, it can forget what it took to
locate the storage. Storage class distinctions evaporate when an lvalue becomes
an rvalue. That sounds more like the arena where const wants to play. You can
see why the committee started out thinking that we wanted a const storage
class.
So this is the peculiarity of const. It wants to qualify type information, so
that it can pop up in all the right places. But it behaves more like a storage
class, since it only aects how you can access a data object with an lvalue.
Thats what I mean when I say that type qualiers lie midway between types
and storage classes.
169
choose to optimize the rst one away. Your program mysteriously fails to emit
carriage returns.
A classic solution in the early days of C was to alter such a program to read:
int *xbuf = (int *)0177566;
*xbuf = \r;
*xbuf = \n;
If you nd that the compiler is not brave enough to optimize out the rst
store, youre done. You leave that code alone and go on to the next problem.
Perhaps years later you upgrade to a new compiler. Only then do you discover
that you have a smarter optimizer to outsmart.
X3J11 wanted some way to end this arms race. We wanted to encourage
aggressive optimization, yet still keep the world safe for people writing lowlevel I/O handlers in C. Our solution was to give programmers a way to mark
those data objects that needed delicate handling. If a translator knew that
other agents could tinker with a given data object, it could be careful not to
optimize away accesses (or move them about rashly). For those data objects,
the translator would generate code that reects the overt intent of the C source.
Just like in the old days.
Thats where the type qualier volatile comes in. You can write it in all
the places where you can write const. You can even mix const and volatile
type qualiers in all possible combinations. Any lvalue that has a volatile type
is handled with kid gloves. The optimizer knows to take a holiday.
Ironically, the great contribution of volatile is when you dont use it. C can
now assume that any data object not marked volatile is the private property
of the translator. It can be much more aggressive about optimizing accesses to
such data objects. It can even advance or retard accesses to nonvolatile data
objects past sequence points. It has much more latitude in generating the same
code as if the sequence points were honored.
volatile is an even odder creature than const. Semantically, it carries no
traditional type baggage at all. Even as a storage class qualier, it is an odd
duck. Dennis Ritchie has never had much good to say about volatile, and
I cant blame him. It is a dirty little piece of pragmatism that doesnt even
deliver completely on any of its promises. Nevertheless, I feel that it provides
a needed service in the real world of C programming.
170
171
to generate the cs: segment override prex any time it dereferenced such a
pointer. You got corresponding behavior with the address space modiers @ds,
@ss, and @es (for the data, stack, and extra segments, respectively). These
were near pointers with no ambiguity. By contrast, the address space modier
@far warned of the need for an arbitrary segment plus oset address. It is
equivalent to the far keyword that has become an institution.
If that were the end of it, the experiment would be just a minor historical
footnote. The near and far keywords popularized by Borland, Microsoft, and
others have clearly prevailed. Programmers have survived without the extra
renements that this family of compilers imposed on near pointers. Fortunately,
the machinery has proved to have other uses as well.
Computers can have any number of address spaces. Even on the Intel 8086
architecture, at least one other space exists. Input and output occur through
a set of ports that have nothing to do with memory. You write in and out
instructions to access these ports. Memory-mapped I/O seldom gets used in
this universe.
To perform port I/O from a C program can be a nuisance. One way is
to call tiny assembly language functions and incur the call/return overhead.
Another way is to write inline assembly code and risk both nonportabilities and
suboptimal code. Address space modiers oer a third and better alternative.
Declare a data object in the space @port and the compiler knows to express
accesses as in and out instructions. It also knows how to optimize arbitrary
expressions containing references to ports, pointers to ports, and so forth.
For ports to work well, another extension is required. You often want to
declare that a data object resides at some absolute address. You can sidestep
the issue by assigning absolute values to pointers, as I showed earlier, but that
is not as nice. So I added yet another bit of notation for dening absolute
addresses:
int xcsr @0177566;
@port char rstat @0x40;
The rst declaration denes xcsr as living at a particular place in memory.
The second denes rstat as the char-sized port number 0x40. (I make no
apologies for the overloaded use of @ to signal various extensions to C. As the
old saying goes, you can always hide it with a macro.)
Other people have used this machinery for even more peculiar extensions.
They really pay o on bizarre little chips that barely support C to begin with.
Performance is much more of an issue and standards conformance much less.
Some of the extensions are:
Zero page addressing, for those small architectures that favor accesses to
the rst few hundred bytes of memory.
Bit vector addressing, for a chip that implements fast Boolean logic in a
special array of bits.
172
You can even qualify the return type of a function to give it special properties. You might want the function to handle interrupts or traps directly. Or
you might want it to be callable from some other language. In either case, you
want the compiler to generate an alternate call/return sequence. You might
even want to promise that a function has no side eects, to encourage a reluctant optimizer. All of these extensions have been expressed with address space
modiers.
Summary
The major point of this article is that type qualiers are a semantic wart on
the language. None of them has very clean semantics, not even const. It is
even hard to craft a general statement about what they all do.
Nevertheless, they have redeeming social value. Type qualiers crop up in
a place where C rubs raw against the real world. They let the programmer
make promises about stu that looks like memory even when it has bizarre
limitations. Thats just the sort of thing you need in the blue collar world of
C programming.
My experience is that generalized type qualiers form a good hook for extending C. You can think of them as a standard way to introduce nonstandard
features into C. Get the semantics right for carrying around any of the type
qualiers and youve got them right for all of them. That goes a long way
toward adding extensions without doing utter violence to the C language.
P.J. Plauger serves as secretary of X3J11, convenor of the ISO C working
group, and Technical Editor of The Journal of C Language Translation. He
recently took over the editorial reins of The C Users Journal. He is currently a
Visiting Professor at the University of New South Wales in Sydney, Australia.
His latest book, Standard C, written with Jim Brodie, is published by Microsoft
Press. He can be reached at uunet!plauger!pjp.
Abstract
In Volume 1, number 2 of The Journal, I outlined the process that the
British Standards Institution (BSI) had undertaken to set up a C compiler Validation service using the Plum Hall C Validation Suite. The
service is also recognised by the European Commission as a harmonised
service. Since that time many things have changed. There is now an
ANSI standard for C and an identical ISO standard is expected to follow in the near future. This paper identies the compilers that are now
validated, describes the problems we have encountered, and how vendors
and other interested parties can participate in the validation process and
the benets of validation.
174
this unusual but highly eective design by using so-called iservers, which
act as the interface between the compiler library and any machine that
INMOS adopt as a development platform. Compiled applications running
on the Transputer also use the same approach, which takes advantage of
INMOSs iserver communications protocol.
Jensen and Partners Ltd; TopSpeed C under MS-DOS
TopSpeed C is a very modern sophisticated PC compiler. It comes with
a fully integrated development environment, and has the usual wealth
of options and features expected of a DOS compiler. This compiler has
already made a big name for itself in Europe partly because of Jensen
and Partners policy that if you are not satised with the product you can
return it and get your money back.
Knowledge Software Ltd, with two compilers:
MCC hosted on PC/MS-DOS to C standard abstract machine
MCC hosted on SUN-4 to C standard abstract machine
MCC stands for Model C Checker. This is actually one of the tools
with which BSI QA measured the standard coverage of the test suite.
The MCC is actually an interpreter-based system. The fact that it is
interpreted allows run time checking of source code. The MCC, in addition to the diagnosing of normal constraint errors, ags all undened
and implementation-dened behaviour listed in the standard. The target is described as C standard abstract machine because the interpreted
environment is the minimum described in the C standard.
Active Participation
One aspect of formal validation, that many C compiler vendors may not be
aware of, is the technical review board. This is the group of people with a
variety of interests that eectively control what goes in and out of the test suite
used for formal validation. In addition, they participate in any appeal process.
If prior to a formal validation a vendor wishes to challenge the validity of a
test then the rst point of call is the technical review board. In addition to
challenges, the technical review board is responsible for reviewing any standard
interpretations and advising on their impact to the test method. BSI QA would
be pleased to hear from any vendors wishing to participate in this process.
Please send email to: [email protected].
USA Validation
In the USA, validation is a slightly dierent aair. The whole process is administrated by NIST, the US National Institute of Standards and Technology
175
176
The introduction of additional requirements by a FIPS can easily be justied, as these publications cover more than just a standard referencethey
also give guidance on interpretation of a FIPS and on validation. However, the
introduction of additional requirements without (I believe) consultation with
X3J11 seems to be misguided. In addition to specifying the Federal requirements for a C processor, the C FIPS also gives contacts within NIST for both
interpretations of the FIPS and validation to the FIPS.
This brings us to the subject of NISTs solicitation for a C validation suite.
In the past, NIST has always managed to obtain the use of a test suite to
measure conformance to FIPS at very low cost to itself. In attempting to
repeat this exercise for C, they issued a solicitation (No. 52SBNBOC6042) to
obtain a test suite for C. There was no need to read between the lines of the
solicitation to realise that the primary purpose of the solicitation was to obtain
a test suite and training in its use for NIST sta at no cost to themselves. While
it is clear that NIST has little money to spend on validation, it is not obvious
that prioritising cheapness over technical merit, or any other feature for that
matter, is benecial to either NIST or the US C community in general.
Abstract
The parse of a text is a sequence of grammar rules applied to source
input. The text is brought into the parser as shift actions and the rules
are applied as reduce actions. The resulting shift/reduce sequence has
some useful properties as an intermediate language for compilers. It is
independent of the parsing technology used to produce it. It can be
stored in a le. It can be incrementally updated. It can be used to
build other intermediate forms such as syntax trees. This paper discusses
some techniques for building and optimizing the shift/reduce sequence.
One such technique has been applied by the authors to an incremental
ANSI C compiler.
Background
The parser is both the best understood and the most central facility of a compiler. The driving loop of the compiler is in the parserit calls the scanner
and drives the generator. For the purposes of this paper, a top-down parser is
a set of mutually recursive routines. A bottom-up parser is a table-driven stack
automaton. Both examine the input text and report its structure. There are
several relatively standard and low-cost ways of building parsers that are error
free and ecient [1, 4].
In the process of attempting to reuse a particular C front end, the authors
found that the output of the existing parser would not serve the intended new
purpose. One alternative was to modify the parser to make a dierent form of
output that would work. Another was to build a new parser. Neither alternative
provided reusein the end there would be two artifacts to maintain. When we
in fact built a new front end, we built output from the new parser that would
have been sucient for both previous uses. It seems as if this result is more
generally reusable, and therefore is documented here.
177
178
Technical Basis
The use of a context-free grammar (CFG) to describe the phrase structure of
programming languages is nearly universal [1]. Nonterminal symbols represent
the main structures of the language. Terminal symbols represent the punctuation (operators, reserved words, etc.) and words (identiers, constants, strings,
etc.) of the language. The implementation representation of a terminal symbol
is called a token. The eect of the scanner is to produce a sequence of tokens.
For C the output of the scanner is preprocessor tokens which are then input
to the preprocessor which produces tokens for the parser. Providing preprocessing is irrelevant for the purposes of this paper. Think of the preprocessor
as part of the scanner.
The parser examines the token stream and discovers and reports the phrase
structure. The two major contending technologies for implementing parsers are
bottom-up and top-down (BU and TD).
A BU parser such as yacc is typically a shift/reduce automaton. There is a
parse stack which is initially empty. Each shift action takes one terminal symbol
from the input and pushes it on the parse stack. Each reduce action applies a
rule from the CFG to the top of the parse stack. The right-hand-side of the
rule consists of a sequence of n terminal and nonterminal symbols; the top n
symbols of the parse stack must match the right-hand-side. The reduce action
pops all n symbols and pushes the left-hand-side of the CFG rule. Parsing
terminates when the input is exhausted and the parse stack contains exactly
one symbolthe so-called goal symbol of the CFG.
A TD parser is a set of recursive routines, each named for a nonterminal
symbol and responsible for choosing and applying the CFG rules dening that
nonterminal. The process begins when the routine for the goal symbol of the
CFG is called. The process descends through further routine calls and eventually returns to the original routine, which by returning, signies that the parse
is complete. There is no explicit parse stack, but the same sequence of shift
and reduce actions implicitly takes place in the call stack.
The eect of the scanner and parser together is completely described by
an interleaved sequence of shift and reduce actions. Suppose the rest of the
compiler, represented by a module called the generator, receives only the shift
and reduce actions via entries:
Shift(t)
Reduce(r)
179
well as complete information on the application of the CFG. This paper proposes the restriction of the post-parser interface to just the two routines, Shift
and Reduce, together with access functions for the abstractions that deal with
the rules and tokens passed by these two routines to the generator. It is our
experience that any dierences in compiler performance caused by following the
structure recommended here are slight.
Tradition
The traditional BU and TD parsers each dier from the proposed solution in
the manner in which they provide storage for the generator. The parse stack,
an internal artifact of the BU parser, is a convenient structure to elaborate and
exploit to save intermediate generator information [2]. The call stack of the
recursive routines in a TD parser provides local variables, which are the corresponding place to save intermediate information. If either traditional storage
technique is used, the parsers are incompatible. The alternative is to provide a
general state saving mechanism in the generator, replacing the traditional use
by the generator of the BU parse stack or TD call stack.
The description of the technique of parser-independent compilation requires
some detailed discussion of functions provided across the compiler interfaces.
There are many ways such interfaces can be dened, and many names by which
the functions can be called. The interface presented here is picked to make the
presentation easy to read. There is no implication that either the names or the
specic choice of functions is optimal.
It is sometimes necessary to go beyond strictly grammatical means of constructing a parse. The variety of such ad-hoc solutions (backtracking for resolving ambiguity, feedback from declarations to the scanner for typedef, etc.)
is beyond the scope of this paper. One can observe that the more regular the
parser, the easier it is to make ad-hoc modications.
180
Each token carries some required information: a lexical code (a small integer identifying which terminal symbol it represents), a textual representation (a
character string), and perhaps also some other less often used information, such
as the line and column in the input where the token begins. Access to the information is provided by routines acting on the value t of function CurrentToken:
c
v
f
n
n
=
=
=
=
=
LexCodeOf(t)
TextOf(t)
FileOf(t)
LineOf(t)
ColOf(t)
and so on. The value t itself is a unique representation for the token upon which
no operations are allowed except assignment and those supplied by the scanner.
The frequency of use of function LexCodeOf is high, indicating that its implementation should be particularly ecient. Both LexCodeOf and CurrentToken
may in fact be macros and/or use hidden local variables to improve performance.
The parser calls through these entries to the scanner. Excepting nonstandard situations (such as caused by Cs typedef), the parsing decisions require
only LexCodeOf(t) for each token t. A TD parser has numerous calls to Scan
and CurrentToken scattered over a number of recursive procedures. A BU
parser needs just one call to Scan and also just one call to CurrentToken to
implement the read-state processing of the automaton it implements. In both
cases the parser may be unable to make some decisions without looking ahead.
As before, a TD parser may have many scattered calls to LookAheadToken
where a BU parser has exactly one call to LookAheadToken to implement the
reduce-state processing of the automaton. The important point is that the
scan/parse interface is the same for both TD and BU parsers.
=
=
=
=
RuleCodeOf(r)
LhsOf(r)
LengthOf(r)
RhsOf(r, i)
181
The generator uses the rule and token information to build the intermediate
representation of the program. The intermediate representation is typically
some form of prex notation, linear pseudo-code, or abstract syntax tree.
The traditional path for the token is from the scanner through the parser
to the generator. For traditional TD compilers, a call to Shift immediately
precedes each call to Scan because that is the moment of acceptance.
Shift(CurrentToken()); send token along
Scan();
discard token
For traditional BU compilers the tokens are already in the (private) parse
stack. Rather than send the token to the generator by calling Shift, the information may be kept in the parse stack and delivered up to the generator
on demand (a pull by the generator from the parser, instead of push by the
parser into the generator). Some generators contain private knowledge of the
layout of the parse stack data structure. Others use a procedural interface to
get at the information, keyed on the match of the top of the parse stack and
the right-hand-side of the rule. For example:
t = ParseStack(2)
might retrieve the token positioned two below the top of the parse stack, and
so on. This technique is not to be used with the parser structure proposed here.
To match the activity of the TD parser, the proposed BU parser must also call
Shift (so it too does a push into the generator).
To summarize, the traditional BU parser calls Reduce. The traditional
TD parser calls Shift. They both use ad-hoc methods for communicating
additional intermediate information to the generator (parse stack versus local
variables in the call stack). The proposed BU and TD parsers must limit
their interactions with the generator to calling only the two routines Shift and
Reduce.
There are two convenient places for a parser to add the calls to Shift. Each
call to Scan can be preceded by a call to Shift, as noted above. Or the scanner
itself can call Shift immediately upon entry to Scan, just before updating the
value of CurrentToken. It is obvious that the eect is the same. When it
is dicult to modify the parser it may be necessary to have the scanner call
Shift.
182
Filtering
Some tokens and some rules have no semantic signicance. That is, they result
in no action in the rest of the compiler. While it can be said that tokens carrying
semantic information, such as identiers and constants, and rules corresponding
to semantic actions, such as arithmetic and branching, are surely signicant,
there is no corresponding concept of surely insignicant. Only the language
implementor knows for sure.
Without loss of generality one can say that the compiler lters the sequence
of tokens and rules, discarding insignicant items. The lter may be placed on
either the sending or receiving end, much as the call to Shift is placed before
or within the call to Scan. At the receiving end, the generator may provide
ltering by ignoring Shift and Reduce when insignicant information arrives.
This is in fact how things end up if nothing special is done.
Another way to lter is for the implementor of the compiler to tabulate
the signicant tokens and rules so that Shift and Reduce omit sending the
insignicant items to the generator. It is slightly more ecient to eliminate
them at the source rather than ignore them later at the destination. The
augmented interface to the generator becomes:
if (SignificantToken(t)) Shift(t)
if (SignificantRule(r)) Reduce(r)
For TD parsers, the test on r above is often computable at the time the
parser itself is compiled. If ltering is on the sending end, the receiving generator needs to compensate by not looking for the missing information.
183
184
Later in the compilation process, the diagnostics report inconsistencies between two sources of information (for example, declaration and use). The tokens
in the shift/reduce sequence provide the basic signposts upon which to establish the locations, although it can happen that an otherwise insignicant, and
therefore ltered, token is signicant to the diagnostic process. The lter must
then pass it.
Conclusion
There are many reasons behind choosing a parsing technique. The point of this
note is not to make the choice, but rather to remove one set of reasons often
cited for making the choice. The proposed solution rules out any criterion based
on the rest of the compiler since the rest of the compiler is independent of the
choice. The proposed solution is also of comparable eciency to traditional
solutions. In any case, the authors believe parsing cost is small compared to
the rest of compilation.
References
[1] Aho, Sethi, and Ullman, Compilers Principles, Techniques and Tools,
Addison-Wesley (1985).
[2] McKeeman, Horning, and Wortman, A Compiler Generator, PrenticeHall (1970).
[3] Pennello and DeRemer, A forward-move algorithm for LR error recovery, Fifth Annual ACM Symposium on Principles of Programming Languages (POPL), pp 241254.
[4] Nicklaus Wirth, Algorithms + Data Structures = Programs, PrenticeHall (1976).
William McKeeman is a Senior Consulting Engineer for Digital. He has
co-authored several books and has published papers in the areas of compilers, programming language design, and programming methodology. He can be
reached at [email protected].
Shota Aki is a Principal Software Engineer at Digital. He was a principal
developer of the VAX APL interpreter. His current interests are in the areas of
CASE, Analysis and Design Methods, Compilers and Tools. He can be reached
at [email protected].
Scot Aurenz is a Principal Software Engineer at Digital and is currently
working on the Language Sensitive Editor project. He can be reached at [email protected].
Introduction
Occasionally, Ill be conducting polls via electronic mail and publishing the
results. (Those polled will also receive an e-mail report on the results.)
The following questions were posed to 90 dierent people, with 21 of them
responding. Since some vendors support more than one implementation, the
totals in some categories may exceed the number of respondents. Also, some respondents did not answer all questions, or deemed them not applicable. I have
attempted to eliminate redundancy in the answers by grouping like responses.
Some of the more interesting or dierent comments have been retained.
Trigraph Recognition
Given that trigraph processing can slow down compilation do you (plan to) have
a compiler option to disable trigraph recognition? (Perhaps you provide a separate utility to convert to/from trigraphs.)
5 Can disable recognition
11 Cannot disable recognition
Comments:
1. Not at the moment. The trigraph processing doesnt seem to be a
particularly expensive part of our compilation time.
2. Turning o trigraph processing based on an option was considered,
but since we dont look for trigraphs inside comments, and comments
represent a signicant portion of the source we are scanning, why
complicate the testing procedures?
3. A separate utility is inappropriate. A compiler should be as strictlyconforming as it can be by default, and trigraphs are, like it or not,
part of the standard. Allowing the user optionally to turn o trigraphs is ne, but making him manually run a separate pass to enable
them is not. Actually, Im not convinced that trigraph processing is
expensive.
185
186
Is main Special?
Does your compiler recognize main as a special function? (Perhaps you do
extra checking or generate dierent code.) Have you ever found a need to call
main recursively?
187
3 main is special
10 main is not special
Comments:
1. Ive never called main recursively but Ive seen programs that have
done so.
2. When main is recognized, the compiler emits a special global which
will cause the linker to pull in the program startup and rundown
code. In addition, this global is dierent depending upon whether or
not arguments are given to main, so that if no arguments are present,
the code to handle command-line processing is not linked into the
program.
3. main is special for us in that our debugger needs additional information to handle it.
4. In many ROMable applications the main entry routine need not save
or restore any registers. However, having a special feature to remove
the few instructions that could be saved here results in very little
savings since the treatment is usually limited to one function in the
entire program.
5. Our MS-DOS version does because it has to emit certain segmentordering information. Our UNIX versions dont.
6. main itself is not special. However, from the loaders perspective,
the library startup code is since it is designated as the primary entry
point of the code. We have a #pragma main name which designates
the function name to be the primary entry point. The function crt0
(main startup code function in our libc) uses this pragma. Users can
designate any other function as the main entry point if they so desire.
188
189
course adding more keywords is not any better. Using more and
more such combinations will only make things much worse. Some
form of extensible specication would be better. For the meantime,
I would think that having long be your wider-than-32-bit integer
and int be 32 bits is a reasonable alternative.
11. long long seems OK to me. When 128 comes around well rethink,
but that will be 20 years.
12. No real opinions here although I cant help commenting that people
with 36-bit machines might want int18 and int36 as well.
13. We have no pressing need for extended types.
14. I cannot support the use of reserved identiers as additional keywords. long long has the advantage that it is a conforming extension. However, I see no real need to access all possible integer
sizes.
15. We already support long long.
16. I do not believe that long long int guarantees a 64-bit integral
type. The best that could be guaranteed is at least 64 bits. Currently, there are four dierent precisions possible for integral types.
This allows 8-bit char, 16-bit short, 32-bit int, and 64-bit long. I
am against this proposal.
17. My preference is to introduce the concept of integer ranges as found
in Pascal. Extremely large integer types can be implemented as
typedefs using large integer ranges. I think that Pascal style ranges
aord better compile-time analysis than the int32 and int64 variants found in FORTRAN.
18. In our implementation, short, int, and long in essence all use 64
bits. Therefore, long long int should not be used to designate a
64 bit object.
Inter-Language Issues
Which other languages does/must your C code interface with? What are the
main problems in establishing correct and ecient inter-language communication? How have you solved them?
Comments:
1. The worst problem is memory managementinterfacing to modern
languages that need garbage collection.
2. Pascal, FORTRAN, and C++. The onus of establishing the communication is mostly on the C compiler/programmer. Our primary
compiler has new keywords to allow the programmer to specify the
190
191
11. FORTRAN, Pascal, and C++. We have pragmas to change calling convention and to specify an object-module name of a function
as opposed to the source name. We also have pragmas to exploit
FORTRAN COMMON blocks.
12. FORTRAN and Pascal. With FORTRAN, there is a big problem
on the user education side concerning the way in which FORTRAN
views parameters. The other major problem is concerned with runtime library initialization: in eect, one language has to be in charge
in a program, and the I/O statements of the other languages cannot
be used.
13. Ada. The main problem here is in passing an exception handler
context. We have not come up with a satisfactory solution to this.
As for Ada calling C, this is not a particularly hard thing to do.
14. F77 and Occam. The function calling convention is dierent, so
special keywords (such as fortran and occam) will be needed.
And the linker will need to be smarter.
15. FORTRAN, Pascal, and Ada (however, for Ada the main entry point
must be in Ada). The biggest problems are between C and FORTRAN. Array storage (column/row major), call-by-reference/value,
TRUE/FALSE, character pointers versus character descriptor, variable dimensioned arrays, and complex arithmetic.
When applied to functions, our fortran keyword implicitly takes
the address of any argument that does not have a pointer type.
fortran.h contains several helpful macros that convert a FORTRAN
LOGICAL into a 0 or 1, convert a C integer into a FORTRAN
LOGICAL, convert a C character pointer and integral length into a
FORTRAN character descriptor, convert a FORTRAN character descriptor into a C character pointer an a length (with unsigned type).
We have extended our C implementation to include variable length
arrays and complex types.
192
Comments:
1. It is under user control via command-line arguments and pragmas.
2. The implementation is controlled by macros in math.h and is triggered by macro denitions on the command-line.
3. All are available to the C programmer by declaring a full prototype
for the intrinsic and using the following pragma:
#pragma XXX intrinsic ( name )
where XXX is the company initials and name is the name of an
intrinsic function known by the compiler. Inside math.h many of
the math functions (such as sin and cos) have this pragma declared
for them. No intrinsic is selected unless this directive is seen.
4. I assume you mean function inlining. pow immediately comes to
mind and (architecture permitting) sqrt, sin, etc. The method suggested in ANSI X3.159-1989, 4.1.6, page 100, footnote 96 suggests
a reasonable approach; in math.h, have something like:
extern double pow(double, double);
#define pow(x, y) _BUILTIN_pow(x, y)
and then have the code generator special-case calls to BUILTIN pow.
This behavior would be the default, but could of course be disabled
by the user by calling (pow)(x, y) or with #undef.
5. We are looking into inlining some popular functions for performance
reasons, e.g., strcpy.
6. Within the standard, I feel free to do as I like here. The worst
problems are for the debugger which has to wend in and around
lots of special code. In any case, I wont do much of any of this
in my compiler since code speed is not the issue in an incremental
environment.
7. Standard C already allows intrinsic functions in that a hosted implementation can know the semantics of a standard library function once
its header is included. If you mean the type-conformable macro-like
FORTRAN functions, they would be a bad idea in C. (Function-like
macros are already available.) A much better approach in C is to extend the language to allow function overloading. This covers a part
of the need. The other dierence between operators and functions
has to do with computational exceptions. This should be handled in
some fashion for existing functions rst. Once that is done, it should
be available for the new overloaded functions.
8. The compiler recognizes lots of library functions since we [the implementors] own them.
193
9. Certain functions are known by our compiler. Most are specic to our
target environment, but some (like memcpy and strcpy) are standard
C functions. These are known even if the programmer forgets to
#include their parent header or puts in his own external declaration
for these functions.
10. We do this with abs, labs, and fabs. Also, most of the is*, str*,
and mem* functions.
11. The transcendental math functions are recognized and converted into
calls to special functions that accept their arguments in registers, and
have a fast entry and exit sequence.
12. We support a collection of intrinsic functions, but they all have reserved names beginning with two underbars. For example, there is
an abs function intrinsic to the compiler. The stdlib.h header
denes an abs macro that maps to abs . This ts the ANSI rules
for overloading of standard library functions and a simple #undef
will selectively disable intrinsics that are not desired for whatever
reason. Also, the rules for expanding function-like macros allow one
to still take the address of the real library function even when the
intrinsic is available for direct calls.
John H. Parks
Compass, Inc.
550 Edgewater Drive
Wakeeld, MA 01880
Abstract
Designing a production quality ANSI C preprocessor was an unexpectedly
complex task. Since K&R (1st Edition) devoted but two pages to its
initial description, one might anticipate that building one would be a
relatively simple matter. It was not, and the ANSI extensions were just
part of the story.
In this paper, I dene our rather lofty goals and discuss our approach
to lexical processing and the processing of macros, #include directives,
and conditional inclusion. I also discuss the extensions we considered and
actually added.
Introduction
We began the project by attempting to dene an ideal C preprocessor. After
considerable study of the ANSI documents and the preprocessors in the eld,
we concluded that an ideal preprocessor should exhibit the following characteristics:
Well dened functionality
Fast
Minimal capacity constraints
Integrated but separable
Sensible error handling
Flexible/portable
One of the largest and most hazardous areas of diversity among C compilers
is in the preprocessor. Implementation-specic features abound, and eorts to
document preprocessor functionality have been notoriously insucient. Quite
frequently, the only reliable way to determine a preprocessors treatment of a
194
195
certain issue has been through testing. This is undesirable. An ideal preprocessor should accept a well dened (if primitive) language and produce well
dened output.
An ideal preprocessor should also be as fast as possible and have few capacity constraints. Too many formals, Actuals too long, Expansion size
exceeded, and other such diagnostics are a common source of irritation for
C programmers developing large software systems, and at times prove to be a
constraint on development. They neednt be. Preprocessing is not inherently
limited by these things, and a well designed preprocessor should be both fast
and capacious.
An ideal preprocessor should also be able to generate an explicit output le.
This does not imply that the preprocessor should generate an output le as a
means of passing its output to the compiler proper. This common technique has
severe performance consequences for the compiler since it requires additional
I/O and a second lexical parse of the input. Instead, it simply means that the
preprocessor should provide the ability to generate such a le when requested.
The ability to generate explicit output les can be extremely useful in debugging and development, especially when macros and header les are used
extensively. It can also allow the preprocessor to be used as a general purpose
tool. Any text le within the broad lexical requirements of the preprocessor
can be eectively preprocessed.
Another characteristic of an ideal preprocessor is that it handle errors in
a sensible and systematic way. The preprocessor should never crash, no matter what the input. Error messages should be descriptive and highlight the
oending sections of input. And the preprocessor should be error correcting
wherever possible. After generating the appropriate diagnostics, the preprocessor should assume a missing parenthesis or comma, complete an incomplete
header name, and so forth. This will yield more complete diagnostic information
and more useful output to the user.
Finally, as with any development tool, an ideal preprocessor should be exible and portable. It should provide switches for commonly found preprocessor
features and allow selective warning message suppression. In sum, it should be
usable on as many systems as possible.
General Description
The Compass C preprocessor is a token based, ANSI-conforming preprocessor.
Because performance is important, it is integrated into the C compiler and
passes tokens directly to the parser, eliminating the additional I/O and lexical
analysis necessary if an output le were rst generated. Macro processing is
optimized, employing sophisticated algorithms and data structures for macro
denition and expansion to avoid copying and rescanning of text. File inclusion
is also optimized, with special treatment given to guarded header les.
High capacity is a requirement, so the preprocessor uses no xed-size data
196
structures. All tables, stacks, and lists are dynamically expandable. Capacity
limits are set by process memory limits, not by program constants. Though
integrated into the compiler, the preprocessor can produce an explicit output
le for debugging or development purposes. It can also be built with a separate
driver to produce a general purpose translation tool with well dened input and
output.
The preprocessor has an extensive and exible set of error handling facilities, and is fully error correcting. Certain diagnostic messages may be selectively
suppressed. Moreover, the preprocessor continues processing the input no matter what the error, making reasonable assumptions about the source writers
intent when an error is diagnosed.
Finally, because portability and exibility are design requirements, the preprocessor contains a full set of switches to allow the user to turn on or o the
more novel and controversial ANSI features, to dene the include le search
path algorithms, to describe the structure and form of the explicit output le,
to enable non-ANSI but popular features found in other preprocessors, and so
forth. The preprocessor also allows the user to dene certain implementation
dened and undened issues from the ANSI Standard.
Lexical Processing
Translation Phases
To clarify the nature of syntactic analysis, the ANSI Standard denes a conceptual model for the translation of source text into tokens. This model is specied
as a sequence of eight distinct translation phases, and denes the order of precedence among the processing rules. The rst four phases are primarily concerned
with the actions of the preprocessor. They may be summarized as follows:
1. Map the physical source le to the source character set and interpret
trigraphs.
2. Delete each sequence of backslash followed by new-line, splicing physical
source lines into logical lines.
3. Recognize preprocessing tokens, white space, and new-lines; convert each
comment into a single space character.
4. Execute preprocessing directives, expand macros, and process #include
directives by recursively processing the named le through these four
phases.
The nal four phases are concerned with translating the stream of preprocessing tokens emitted by the preprocessor into actual tokens and then eventually into a program image.
197
Trigraphs
The ISO-646 Invariant Code Set, an internationally agreed upon character set,
is not rich enough to express the C language in full. Nine essential characters
are absent. The ANSI committee introduced trigraph sequences as alternate
spellings for the nine missing characters to allow the implementation of C in all
ISO-646 conforming character sets.
As it is dened as a separate pass over the source text, trigraph processing
can be costly. There are two obvious implementation choices: create a lter to
screen trigraphs before source text is fed to the lexer, or incorporate trigraph
processing directly into the lexer/preprocessor. The rst choice is undoubtedly
expensive. It requires that every character of the source input be read at least
one more time, and complicates the task of pointing to the oending section of
code when errors are found. The second choice has neither of these problems.
We chose the second.
Sophisticated Lexer
We combine the rst three translation phases (and the three scans of the stream
input that they imply) into one sophisticated lexical analysis phase. This phase
inputs source text and outputs a stream of preprocessing tokens, each containing
source position information and a bit describing whether or not white space
appeared before it in the source text.
This is accomplished with a standard nite-state machine, generated from
a regular expression grammar like that given in the ANSI Standard, but with
the following modications:
1. Trigraphs are included as alternate representations for the aected characters. For example:
TOKEN_LBRACKET
[ | ??(
2. Fixed multi-character tokens allow arbitrary sequences of backslash followed by new-line. This includes, of course, the trigraph representation
of backslash. For example:
BACKSLASH
BNL
TOKEN_EQUAL_EQUAL
\ | ??/
BACKSLASH NEWLINE
= BNL* =
3. Variable length, multi-character token classes are described by two different tokens, one for tokens that contain embedded backslash/new-line
sequences and one for tokens that do not. So, for instance, there is a
TOKEN IDENTIFIER and a TOKEN IDENTIFIER BNL, a TOKEN STRING and a
TOKEN STRING BNL, and so forth.
198
Comment Processing
Comments do pose something of a problem for the generated lexer. The regular expression grammar needed to recognize C comments is non-trivial even
without the added complexity of trigraphs and backslash/new-lines because the
closing delimiter must be excluded from the body of the comment. Allowing
arbitrary sequences of backslash/new-lines to appear between the * and / of the
closing delimiter complicates the grammar immensely, rendering it practically
intractable.
We solve this problem by recognizing only the opening comment delimiter
in the lexer. The preprocessor then recognizes the closing delimiter itself. This
is relatively simple since it is looking for a pattern to terminate the comment
rather than trying to nd the longest match to a pattern that matches everything except the terminating sequence. This solution has the advantage of
allowing the preprocessor to write the text of comments to an explicit output
le very easily at the users request. The preprocessor simply writes out the
comment text while scanning for the terminating sequence. Were the full comment recognized by the lexer, its text (of unbounded, and often very large size)
would need to be buered by the lexer and then rescanned to write it to an
output le.
Macro Processing
Macro processing involves macro denition and macro replacement. Macro definition is computationally simple. There are interesting optimization issues to
199
resolve involving how macro denitions should be stored and how redenitions
should be examined, but the work is not complex. Macro replacement is another
matter. It may be quite complex, confusing, and counter intuitive. Macros may
nest within other macros. Macro calls may be built on the y using formal
parameter substitution. Macro replacement, or token pasting, and some limited
forms of recursion are allowed.
Designing a fast macro processor that scans each token as few times as
possible, that does minimal list shuing, and that is bound by minimal capacity
constraints is a non-trivial task.
Expansion Algorithm
A straightforward implementation of the above would rst produce and store
the replacement list for every actual parameter that needs expansion (Pres-
200
can). It would then scan the macro body and substitute these replacement lists
where appropriate (Substitution). Finally, it would rescan the macro body to
perform stringizing and token pasting operations and to expand macros that it
encounters (Postscan).
This sketch describes an algorithm that requires two complete scans of the
macro body, storage for the expanded forms of actual parameters, and storage
for the fully expanded macro body. Recognizing these costs, we chose another
approach.
Our approach expands macros in a single left-to-right pass over the macro
denition. It builds no intermediate token lists except for unexpanded actual
parameters, and it outputs each token as it is determined. The state of the
expansion is preserved while control is returned to the parser so that expansion
can resume the next time it is called. The costs of this solution are algorithmic
and data structure complexity. It must accomplish in one pass what is specied
in two, and must represent an essentially recursive expansion process in static
data structures.
201
the macro table. Macro denitions are stored as token lists, and so interesting
opportunities for denition time token transformations present themselves.
We move token pasting operators in front of their left operands (transforming token ## token into ## token token) to remove the macro expansion burden
of looking past each token to determine if it is token pasted. We also transform
stringizing operations(# followed by a formal parameter) into single stringizing
tokens. This has the nice consequence of distinguishing stringizing operators
from ordinary # tokens. These are simple transformations triggered by specic
tokens; they seem worthwhile to do while building the denition.
We do not mark formal parameters as such at denition time, however.
Quite often in real code a substantial percentage of the macros dened in header
les are never called within a particular compilation. Since nding the occurrences of formals within a macro body involves some type of lookup for each
identier, it makes sense to do this processing only for those macros that are
actually called. We decided, then, to mark formals as such during the rst
expansion of each macro. Macros that are never called are then cheaper, and
macros called more than once have no added burden.
Include Processing
The #include preprocessing directive allows the user to include header les in
the preprocessor input. It has three variations: the system le variation, the
user le variation, and the computed header name variation. In the latter, the
argument tokens are macro expanded and combined to form one of the two
other forms.
According to ANSI, including a le should produce the same results as copying the contents of that le into the original, with the following three exceptions:
comments may not begin in one le and end in another, and backslash and
new-line may not be the nal two characters in any le (tokens cannot span le
boundaries), and related conditional inclusion directives must all reside within
one le.
Implementation
Because header les may nest one within another, we chose an obvious, stack
based algorithm for include processing. There were just two sticky issues, header
name determination and include search strategy. The lexical denitions for
header names conict with the lexical specications for the C language proper.
It is thus impossible to use a common grammar for both. This adds complexity
to the lexical analysis process.
The other sticky issue was include le search strategy. The ANSI document
does not elaborate on how or where the preprocessor should search for header
les. This is left for the implementation to dene. Assumptions about operating
systems and le storage are also carefully avoided.
202
Given that extant preprocessors vary signicantly from one another in this
area, we felt that the best solution was to allow the user to dene search strategies for both system and user header les from the command line. The user
may dene a set of places to search and an order in which to search them. This
also involves dening whether or not the search should begin in the location
of the original source le or the location of the (include) le presently being
processed.
/* FILE_GUARD */
Notice that if this le is included a second time, the preprocessor will not
process macro denitions or pass tokens on to the parser or explicit output le.
It will, however, read in and process the le, at a non-trivial cost.
Noticing this cost, we designed the preprocessor to specically recognize
guarded header les. When it nds one, it saves the guard for future reference.
Then, if the le is included again, the preprocessor can process the guard without having to input the le. If the guard test fails, the le is dismissed without
having to be input. The savings can be quite considerable.
Conditional Inclusion
The preprocessors conditional inclusion facility allows the user to include or
exclude source code at compile time, an important ability when developing
portable applications. ANSI has dened six preprocessing directives for this:
#if, #ifdef, #ifndef, #else, #elif, and #endif. They may nest one within
another giving the user substantial exibility.
Constant expressions in this context are somewhat interesting. All operands
are either long or unsigned long and identiers that are not macros are interpreted as 0L. The grammar includes the new ANSI defined operator, but
excludes the sizeof operator and casts.
Our implementation utilizes a stack based algorithm to control directive
nesting and a recursive descent parser/evaluator to process constant expressions. Unfortunately, the grammar diers enough from the C constant expres-
203
sion grammar to make reuse of that evaluator dicult, and so the preprocessor
has its own.
Token Concatenation
Designing a preprocessor to produce an explicit output le introduces a new
measure of correctness. It seems reasonable to assert that a correct preprocessor
should be consistent with itself to the extent that when it produces explicit
output and that output is then fed back into the compiler, the result should
be identical to that produced when no explicit output is generated. This seems
like an appropriate objective, and it would not be unreasonable for a user to
expect this, but the objective can be extremely dicult to obtain. Consider
the following example:
#define f(x) hello
f(1)there
When preprocessed, this produces hellothere. ANSI species that this
result is actually two tokens with no intervening white space. But if an output
le were generated and then fed back into the preprocessor, just one token would
result. This eect is not limited to identiers, but can occur whenever macro
expansion places concatenatable tokens adjacent to one another in preprocessor
output.
One common solution is to insert a space character after every macro expansion written to preprocessor output. This would solve the problem in the above
test case. This solution is incomplete, however, as evidenced by the following
two examples:
204
205
Conclusion
Building a production quality ANSI C preprocessor was a more complex task
than we had anticipated.
Many extant preprocessors restrict functionality to gain performance and
simplicity. An ANSI conforming preprocessor, however, cannot. It must process the language as specied. It is common, for instance, for preprocessors to
restrict directives to begin in column 1 to simplify directive recognition. An
ANSI preprocessor, though, must recognize directives in any column (preceded
by new-line and horizontal white space). Simplifying assumptions are not allowed.
Our design was complicated by our requirement that the preprocessor be
backward compatible with others in the eld. The preprocessor had to handle
both old style and new style features where dierences arose. Two stringizing strategies were necessary, for example, two token pasting strategies, and a
number of le inclusion algorithms.
We also required that the preprocessor be fast. Since relexing after preprocessing is a serious performance penalty, this requirement seemed most compatible with a single pass, token based design. Creating an explicit output le,
however, was somewhat dicult in this setting. The integrity of tokens can
easily be lost, if care is not taken to prevent it. White space information can
also be lost rendering the output dicult to read. Getting this right was a
non-trivial task.
The requirements we set down at the outset of the project pushed us towards
an interesting and complex design. Optimizations abound, and the tool is
206
Jim Brodie
Abstract
In this article, I will continue the review of C Standard interpretation
requests being addressed by X3J11, the standards committee that developed the American National Standard for C. In particular, I will explore
the issues surrounding one of the most dicult and interesting requests.
207
208
=
=
=
=
1;
2;
3;
4;
209
imaginable.
The members of structure u.s1.s overlap with the members of u.s2.s as
shown in the following diagram:
u.s1.s
u.s2.s
The u.s2.s members are oset by one due to the addition of the int-sized
element t into the u.s2 structure denition. Because u.s1 and u.s2 are part of
a union, the members u.s1.s.j and u.s2.s.i share the same physical memory.
This also holds for u.s1.s.k and u.s2.s.j as well as u.s1.s.l and u.s2.s.k.
The partial overlap means that the act of writing members of one structure
may interfere with the reading of the members from the other. The function call
optimization has the eect of treating the assignment portion of the statement:
u.s2.s = ret_struct(&u.s1.s);
as if it were the assignment:
u.s2.s = u.s1.s
The problem that can arise in the assignment revolves around the fact that
the u.s1.s structure may be (and most probably is) copied a piece at a time
into the u.s2.s structure. A structure assignment algorithm may do the structure assignment by copying each element, starting with the rst element and
proceeding, element by element to the last element. Lets see what happens
when this approach is used when the source and target partially overlap.
Assume that the members i, j, k, and l of u.s1.s are initialized with 1, 2,
3, and 4, respectively. The copy starts by moving the value 1 from u.s1.s.i to
u.s2.s.i. Because of the overlap caused by the union, this also has the eect of
setting the value of the u.s1.s.j to 1. So when the second step of the structure
assignment occurs, the copying of u.s1.s.j to u.s2.s.j, the newly set value
of 1, rather than the original value of 2, will be copied. When the structure
assignment is complete, all of the members of u.s2.s will have the value 1. This
is probably not what was intended by the author of the program. (By the way,
if you think that this problem could be solved by copying in the other direction,
consider the case where the operands of the assignment expression are reversed.
In that case the reverse order copying would produce similar results.)
In the partially overlapping case, the resulting behavior of the direct structure assignment expression is undened according to the Semantics subsection
210
211
212
213
214
precedence rules, the function call must be completed prior to the assignment
operation. In particular, the functions return statement must have been completed prior to the assignment operation. As noted above, in the description
of the return statement, the return statement evaluates its argument prior to
returning from the function, to produce a value.
Despite the committees focus on the impact of sequence points in this case,
sequence points may be the wrong place to be looking for the answer. Sequence
points address the timing of modications of the program state. They do not
deal directly with the issue of when the production of a value (as a result of an
expression evaluation) is complete. The guarantees in this area are addressed
in 2.1.2.3, Program Execution, of the Standard, which states:
The semantic descriptions in this Standard describe the behavior of
an abstract machine in which issues of optimization are irrelevant.
Later in that section is the paragraph:
In the abstract machine, all expressions are evaluated as specied
by the semantics. An actual implementation need not evaluate part
of an expression if it can deduce that its value is not used and that
no needed side eects are produced ...
It seems, at least to me, that the function return by reference is a way of
delaying the evaluation of the return value expression. Since this value is used
in the assignment statement once the the function call is complete, it seems
that the freedom given in the statement An actual implementation need not
evaluate part of an expression if it can deduce that its value is not used does
not apply.
These points argue that the return by reference approach is invalid. Of
course, one persons opinion does not make an interpretation response. I will
continue to report on this on-going saga as X3J11 continues its deliberations.
215
change anything in this area because of the future decisions of the committee.
(If X3J11 rules that the partially overlapping case results in undened behavior,
you will simply have a product that does something nice in this undened case.
If they decide that the partially overlapping case must be supported, you will
already supply the appropriate support.)
At this point, implementors have to evaluate the issues, determine what they
feel the likely nal position will be, and then weigh this against the competitive
cost of less than optimal code versus the cost of removing this optimization
from a future release of their product.
An ISO C Update
By the time this article is published, there will possibly be an approved ISO
(International Standards Organization) Standard for the C programming language. It will be technically identical with the American National Standard.
However, the formatting rules for International standards prevents the identical
text from being used.
The ISO Standard will guide the development of C programs and translators
throughout the world. The on-going international work on Normative Addenda
will, once it is developed and approved, modify this Standard. It is interesting
to note that the single largest collection of interpretation requests has come
from Derek Jones of England. Derek is heavily involved in the development
of the (ISO) Normative Addenda. As a leader in this eort, he has brought
forward many of the issues the UK has with the ANSI Standard so that he
can ensure a clear understanding of the X3J11 view. This is important, since
the stated intent of the UKs Normative Addendum has always been editorial
clarication rather than making substantive changes to the Standard.
Ed: If any readers have thoughts on how this issue might be resolved they
are encouraged to contact Jim (or any other X3J11 member) as indicated below.
Jim Brodie is the convenor and Chairman of the ANSI C standards committee, X3J11. He is a Senior Sta Engineer at Honeywell in Phoenix, Arizona. He
has coauthored books with P.J. Plauger and Tom Plum and is the Standards
Editor for The Journal of C Language Translation. Jim can be reached at (602)
863-5462 or uunet!aussie!jimb.
25. Pragmania
Rex Jaeschke
message
list
execution
german
iso_latin_1
italian
norwegian
portuguese
spanish
swedish
swiss
dutch
finnish
french
french_canadian
By default, all four categories are iso latin 1 and each category may be
specied independent of the others. If the category is omitted, all four categories
are assumed.
A header (including nested headers) assumes the same character set as its
including le. The eect of any character set specied within a header is,
however, implicitly disabled at the end of that header.
String literals and character constants are translated to the execution character set.
charset pragmas allow you to use older, 7-bit input/display devices that
assign certain character positions to represent certain national-specic glyphs.
216
Pragmania Jaeschke
217
For instance, the ASCII decimal code 92 normally represents the backslash (\).
However, the 7-bit French National Replacement Character set (NRC) replaces
this position with the French c-cedilla (c). Consider the case where program
development is performed using an ISO Latin-1 8-bit input/display device that
does not require character set translation, but where the program is to display
text on a 7-bit French NRC terminal at run time. PDP-11 C supports this as
follows:
#include <stdio.h>
#pragma charset execution french
int main (void)
{
printf ("Ici, on parle Fran
cais.\n");
}
If the pragma were omitted and this program run on a 7-bit French NRC
terminal, the message would display as follows:
Ici, on parle Frangais.
The c in Francais is displayed as g because the high order bit of the 8-bit
c (ASCII decimal code 231) is stripped, resulting in g (ASCII decimal code
103). The result is far from gratifying to a Frenchman. The pragma directs the
compiler to translate the characters in the string literal to correspond to the
French NRC, and the result appears on the French NRC terminal as follows:
Ici, on parle Fran
cais.
The Swiss NRC replaces the character with e. This is contrary to ISO
standards for NRCs, but they do it anyway. And because the Swiss approach is
not sanctioned by an ISO standard, it is not covered by the trigraphs invented
by ANSI C. Thus, if you specify your source character set as Swiss, there is
no way to represent the underscore character in your programa new trigraph
would be needed. However, it is presumed that most program development
will be done using 8-bit DEC MCS or ISO Latin-1 input/display devices and
that this pragma will be used primarily for run-time and listing display devices.
(DEC MCS is the character set used by Digitals VT200 series of terminals and
later. It pre-dates the establishment of ISO Latin-1 and is almost identical.
Most C programs need not consider the minor dierences between DEC MCS
and ISO Latin-1. While PDP-11 C can translate between DEC MCS and ISO
Latin-1, it is usually not necessary.)
The ability to specify dierent character sets for source les or compile-time
messages may be desirable for some customers. Except for the case of when
using the Swiss character set for source les, trigraphs allow all characters used
in C to be represented. Note that using the Swiss character set for run-time
218
list
list
list
list
on
off
title
subtitle
"..."
"..."
The list on/o switch is cumulative. That is, it is a counter that starts at
zero and is incremented for each list on and decremented for each list off.
A listing is produced so long as the counter is positive.
Inter-Language Communication
A problem common to many implementations is that of linking together object modules produced from dierent (and somewhat incompatible) transla-
Pragmania Jaeschke
219
tors. Unlike the VAX, the PDP-11 has no standard calling convention. As a
result, dierent compiler and utility design groups chose dierent register and
stack layouts for argument passing. To help resolve these conicts, PDP-11 C
provides the linkage pragma whose syntax is as follows:
#pragma linkage linkage type [ function1 [, function2 ] ... ]
where linkage type species a language such as c, pascal, or fortran.
The functions whose names are listed are assigned the corresponding linkage.
If the function list is omitted, the linkage type specied becomes the default for
all the functions that follow. Linkage pragmas remain in force until overridden
by another linkage pragma.
TopSpeed C V1.04
[Ed: The following information is taken from JPIs documentation which is
c 198990, by Jensen & Partners International. It is reproduced here with
their kind permission.
TopSpeed C is a member of a series of MS-DOS based languages from Jensen
and Partners International (JPI) that conform to international standards. Of
these, Ada is the only one that has a formal denition for a pragma syntax. As
such, JPI chose the ocial Ada standard syntax. This has the advantage that
all TopSpeed languages share the following pragma syntax:
#pragma pragma_name( parm1 => val1 [, parm2 => val2, ] ... )
For example:
#pragma call( c_conv => off, near_call => off )
turns standard C calling conventions o and near calls o. The next example:
#pragma call( reg_param => (ax,bx,cx,dx) )
220
tells TopSpeed C to use the indicated registers for parameter passing to functions.
The set of pragmas are broken into the following groups:
Category
call
check
data
link
name
option
save
warn
Some Highlights
While there are many many pragmas dened by TopSpeed C, only a few of
them will be discussed here in detail.
call Specify whether arguments are passed by register or stack. For register,
the registers used and which ones should be preserved. For stack, the
order pushed and who cleans up the stack.
check Run-time checking for dereference of NULL, invalid array index access,
and stack overow.
data Can establish stack and heap size and dene shared globals in a multithread environment.
name Can add a user-specied global name prex and specify if external
names are to be case-signicant.
save You can save an entire pragma state for later restoration. Since a stack
mechanism is used, pragma states may be nested.
warn Numerous very useful lint-like warnings can be produced, for example:
No expression in return statement.
No return from non-void function.
Function called and not declared.
No prototype in scope of call.
Code has no eect.
Assignment in test expression (e.g., if (x = y)).
Returned address of an automatic variable.
Pragmania Jaeschke
221
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
on o
seg name
on o
on o
on o
on o
reg list
reg list
reg list
on o
seg name
on o
222
=>
=>
=>
=>
=>
=>
on o
number
on o
seg name
on o
number
Pragmania Jaeschke
223
param
param list , warn param
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
on
on
on
on
on
on
on
on
on
on
on
on
on
on
on
o
o
o
o
o
o
o
o
o
o
o
o
o
o
o
err
err
err
err
err
err
err
err
err
err
err
err
err
err
err
224
wpcv
wpic
wpin
wpnd
wpnu
wprg
wral
wrfp
wsto
wtxt
wubd
wvnu
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
=>
on
on
on
on
on
on
on
on
on
on
on
on
on o err ::=
err
off
on
o
o
o
o
o
o
o
o
o
o
o
o
err
err
err
err
err
err
err
err
err
err
err
err
Tom MacDonald
Cray Research, Inc.
655F Lone Oak Drive
Eagan, MN 55121
Abstract
The initial meeting of the NCEG committee identied and assigned priorities to several key issues that needed to be addressed. C aliasing was
identied as being the highest priority issue because of its eect on optimization. One approach being explored is a new kind of pointer called
a restricted pointer. A restricted pointer gives the compiler the liberty
to assume that the pointer behaves as if it were an array for aliasing
purposes.
The array analogy is being explored because optimizations involving
arrays are understood, and programmers can view this new kind of pointer
in terms of an existing concept. Several new areas must be explored
though, because pointers can be used in ways that arrays cannot. The
greatest benets of restricted pointers are achieved when they are used
for function parameter declarations, or when they are the targets of calls
to dynamic memory allocation functions.
Introduction
In the September 1989 issue of The Journal (Volume 1, number 2 ), I wrote
an article, Aliasing Issues in C, in which I discussed the optimization issues
associated with the unconstrained aliasing present in C. I alluded to a new kind
of pointer that would help optimizers. This article is a follow on to that one.
It proposes a new kind of pointer that ameliorates C aliasing.
The following two terms are used throughout the rest of this article:
reference access or modify an object
lvalue an expression that references an object
The presence of unconstrained pointers in C introduces aliasing that inhibits
many useful optimizations. Two of these optimizations are automatic vectorization and automatic parallelization. On a single processor CRAY Y-MP, a
typical vector loop runs ten to twenty times faster than the equivalent scalar
225
226
#include <stdio.h>
int a[6] = {0, 1, 2, 3, 4, 5};
int b[6] = {9, 8, 7, 6, 5, 4};
int c[6];
void blackbox(int *p1, int *p2, int *p3, int n);
main() {
int i;
blackbox(c, b, a, 6);
/* no aliases */
for (i = 0; i < 6; i++)
printf(" c[%d] = %d ", i, c[i]);
putchar(\n);
blackbox(&a[1], &a[1], a, 5);
/* aliases */
for (i = 0; i < 6; i++)
printf(" a[%d] = %d ", i, a[i]);
putchar(\n);
}
void blackbox(int *p1, int *p2, int *p3, int n) {
int i;
for (i = 0; i < n; ++i)
*p1++ = *p2++ + *p3++;
}
227
228
If there were some way to indicate that pointers a, b, and c are never aliases
for each other, and behave just like arrays, then optimizations similar to those
permitted in the rst case would also be permitted in the second.
The proposed semantics for a restricted pointer is that it behaves just like
an array when being viewed by the optimizer. That is, if an optimization
can be applied to an array reference, the same optimization can be applied
to a restricted pointer reference. The proposed syntax for restricted pointers
involves a new keyword, restrict, and syntax similar to that of the type
qualiers const and volatile.
double * restrict p;
The semantics of this new type of pointer involves only assertions about
aliases. The primary motivation is to allow the compiler to perform more
optimizations. If the compiler is not optimizing, then the semantics of restrict
can be ignored (just like volatile).
The semantics of restricted pointers needs to capture the concept that aliasing assertions, that are correct for arrays are also correct for restricted pointers.
The array characterization is used because it allows the compiler and the programmer to view the program in the same way. The blackbox function can
now be written in the following way:
void blackbox(int * restrict p1, int * restrict p2,
int * restrict p3, int n) {
int i;
for (i = 0; i < n; ++i)
/* no hidden aliasing */
*p1++ = *p2++ + *p3++;
}
The compiler can now assume that the restricted pointers p1, p2, and p3
behave like distinct arrays for optimization purposes. This means the following
call:
blackbox(&a[1], &a[1], a, 5);
/* aliases */
229
Array Semantics
Many optimizations require the optimizer to know how far lvalues can move
relative to other lvalues. Considerable latitude is given to the optimizer in
reordering the following array references because they are lvalues that reference
disjoint objects.
#include <stdlib.h>
int a[10];
void f1(int i) {
static int b[10];
int c[10];
int *p1;
p1 = malloc(10 * sizeof(int));
a[i] = i;
b[i] = i;
c[i] = i;
p1[i] = i;
}
It is perfectly acceptable for the optimizer to reorder the assignments as
follows:
#include <stdlib.h>
int a[10];
void f1(int i) {
static int b[10];
int c[10];
int *p1;
c[i] = i;
b[i] = i;
p1 = malloc(10 * sizeof(int));
p1[i] = i; /* cannot be moved before malloc */
a[i] = i;
}
230
These reorderings just aect the order in which assignments are performed
on independent objects. This is acceptable because the semantics of these arrays
is such that the optimizer knows where their storage resides, when the storage is
allocated, and just how far loads and stores to these objects can be moved. For
instance, the assignment through pointer p1 cannot be moved before the malloc
call that allocates the array. They are all arrays and, therefore, independent
objects. Since optimizations like automatic vectorization must reorder array
references, the optimizer must know how far they can be moved. The following
example replaces all array declarations with restricted pointers.
#include <stdlib.h>
int * restrict a;
void f2(int i) {
static int * restrict b;
int * restrict c;
int * restrict p1;
p1 = malloc(10 * sizeof(int));
a[i] = i;
b[i] = i;
c[i] = i;
p1[i] = i;
}
Since a restricted pointer is just like an array, these assignments can be
reordered by the optimizer in similar ways.
C is a block scoped language, with blocks delineated by { and } tokens.
Two blocks are considered to be siblings if they have the same parent block.
The next example shows some dierent behavior for dierent arrays dened in
sibling blocks.
void f3(int i) {
/* parent block
{
static int m[10];
int n[10];
m[i] = i;
n[i] = i;
}
*/
/* sibling block 1 */
/* overlaps with y */
231
{
static int x[10];
int y[10];
/* sibling block 2 */
/* overlaps with n */
x[i] = i;
y[i] = i;
}
}
The auto arrays n and y are allowed to share the same physical memory
location. That is, it is quite common for compilers to allow auto variables
to overlay the same physical memory as auto variables in sibling blocks. This
means the optimizer must not move assignments to array y before any references
to array n. In a way, they are aliases with each other. This is referred to as
the auto array model. However, arrays m and x are static arrays and do not
share the same physical memory locations. The assignment to x can be moved
before the assignment to m. This is referred to as the static array model. The
following is an acceptable reordering of assignments by the optimizer (though
one could not write C code this way).
void f3(int i) {
/* parent block
x[i] = i;
{
static int m[10];
int n[10];
*/
/* OK to move here */
/* sibling block 1 */
/* overlaps with y */
m[i] = i;
n[i] = i;
}
{
static int x[10];
int y[10];
/* sibling block 2 */
y[i] = i;
}
/* cannot move
/* overlaps with n */
*/
}
This raises the question about which model to follow. In the following
example, should the two restricted pointers p and q, declared inside the sibling
blocks, follow the auto array model or the static array model?
232
There is a third model that can be used to describe the behavior of restricted
pointers declared in local blocks. A restricted pointer behaves as if it points
into memory obtained from a call to malloc. 4.10.3 (Memory Management
Functions) of the ANSI standard guarantees that each call to malloc shall
yield a pointer to an object disjoint from any other object. This allows the
optimizer to assume that lvalues derived from restricted pointers are never
aliases with lvalues derived from arrays. This is called the malloc array model.
int a[10];
void f5(int i) {
{
int * restrict p /* = malloc(N) */;
/* cannot be moved above implied malloc call */
a[i] = p[i];
}
{
int * restrict q /* = malloc(N) */;
/* cannot be moved above implied malloc call */
a[i+1] = q[i];
}
}
The optimizer can assume that p and q point into disjoint arrays that were
allocated by malloc. Therefore, neither points into array a. The malloc calls
are hidden inside comments, to show the implied behavior the optimizer is
relying upon. The simplicity of the as if the array came from a malloc call
makes this model easy to understand and straightforward to dene.
Another eect of using this model is that the aliasing assertions of restricted
pointers are conned to the scope that contains the declaration. They are only
in eect when the declaration is visible. There are no aliasing assertions if the
restricted pointer is not visible. This is part of the behavior guaranteed by
the implied malloc call. Therefore, neither of the two assignments present in
233
function f5 can be moved outside the block that contains the declaration of the
restricted pointer.
It is important to understand that the implied malloc calls are not part of an
alternate execution model for programs containing restricted pointers. Rather,
they represent aliasing assumptions that only the optimizer can use for purposes
of statically analyzing the program. It is the programmers responsibility to
ensure that these assumptions are correct. The following example shows both
well dened behavior and undened behavior.
void f6(int i) {
i = 1;
/* defined behavior */
{
int * restrict p = &i;
*p = 3; /* defined behavior */
}
{
int * restrict q = &i;
i = 3; /* i and *q are not disjoint */
*q = 2; /* undefined behavior */
}
}
The assignments to object i occur both through i and through two restricted
pointers that point to i. The undened behavior exists when the assignment
through i occurs in a context where either of the two restricted pointers is visible. The behavior is undened because the optimizer is at liberty to reorder the
two assignments. If neither of the two restricted pointer is visible, assignments
to i are well dened.
234
/* no aliasing */
/* no aliasing */
}
For these two purposes, the aliasing assertions work very well. This alone
makes the restricted pointer proposal appealing because these are the areas in
the language where restricted pointer semantics are needed the most. However,
the just like an array paradigm needs to be explored in areas of the language
where pointers can be used in ways that arrays cannot. This is necessary to
achieve closure on the language for restricted pointers.
One of the primary purposes just identied for restricted pointers is to
indicate that formal parameters are not aliases with each other, as in:
void blackbox(int * restrict p1, int * restrict p2,
int * restrict p3, int n);
However, C does not permit a formal parameter to be declared as an array.
What does it mean to say that formal parameters behave just like arrays? Is it
meaningful to say that the implied malloc call occurs inside function prototype
scope? The question becomes, does the implied malloc call occur before the
function is called or after the function is entered? The answer matters when
one parameter is declared to be a restricted pointer and another is declared to
be an unrestricted pointer.
void f8(int *p, int * restrict q) {
/* is the behavior as if q=malloc(N)? */
int i;
for (i=0; i<10; i++)
p[i] = q[i];
}
235
236
If the keyword restrict were left out of the return type, the function would
have the same semantics. Similarly, no aliasing assertion is made by restricting
a pointer to a function, since such a pointer does not point at an object.
/* no aliasing assertions */
int (* restrict pf)(void);
Conclusions
There is no substitute for an actual implementation. The true worth of the
restricted pointer proposal will be determined when a compiler is available that
can be tested. Several things must happen before restricted pointers are considered a success. First, there must be improved execution times of applications
modied to use restricted pointers. Second, programmers must be willing to
use restricted pointers. And, nally, the semantics must be understandable
and sensible. It must be easy to understand when it is safe to use a restricted
pointer and when it is appropriate to use the traditional unrestricted pointer.
A restricted pointer is only useful for optimization purposes. At the point a
restricted pointer is declared, the optimizer is allowed to assume that it points
237
into an associated array allocated by an implied call to malloc. This means the
optimizer can assume that the associated array is an object disjoint from all
other objects. Furthermore, the aliasing assertions are only meaningful while
the declaration of the restricted pointer is visible.
I would like to thank my colleagues Bill Homer and Steve Collins for their
input into this article.
Tom MacDonald is the Numerical Editor of The Journal of C Language
Translation. He is Cray Research Incs representative to X3J11 and a major
contributor to the oating-point enhancements made by the ANSI standard.
He specializes in the areas of oating-point, vector, array, and parallel processing with C language and can be reached at (612) 683-5818, [email protected], or
uunet!cray!tam.
27. Miscellanea
Extensions
MetaWares High C
High C contains a number of interesting extensions, most of which were inuenced by Pascal and Ada.
The compiler version used for this article was R2.3c running under MS-DOS
on an Intel 80386. It runs in 32-bit native mode where the types int, long,
and all pointer avors are represented in 32 bits.
Case Ranges
Non-overlapping case ranges are permitted with switch as follows:
#include <stdio.h>
main()
{
int c;
while ((c = getchar()) != EOF) {
switch (c) {
case a..m:
printf("a-m: %c\n", c);
break;
case 0..9:
printf("0-9: %c\n", c);
break;
default:
printf("default:\n");
break;
}
}
}
238
Miscellanea Jaeschke
239
240
or the leading arguments have no such association and are interpreted according
to their position and trailing arguments have explicit association. For example:
#include <stdio.h>
main()
{
void f(char c, int i, double D, char *pc);
int j = 10;
double b = 1.2;
char *p = "abcd";
f(x, D =>b, pc => p + 1, i => j * 5);
}
void f(char c1, int i1, double D1, char *pc1)
{
printf("c1 = %c, i1 = %d, D1 = %f, pc1 = %s\n",
c1, i1, D1, pc1);
}
c1 = x, i1 = 50, D1 = 1.200000, pc1 = bcd
In this case, function f is called with 4 arguments. The rst has no explicit
association and is therefore interpreted as corresponding to the rst formal
argument (char c) in the prototype. The trailing arguments are associated
with the remaining formal argument names using the => notation. As you
would expect, the case of the formal argument identiers is signicant. However,
the names used in the function denition do not have to match those in the
prototype.
Nested Functions
High C permits functions to be nested, as follows:
#include <stdio.h>
main()
{
void f(void);
f();
}
Miscellanea Jaeschke
241
void f(void)
{
void g1(void);
void g2(void);
static int i = 10;
puts("Inside f");
void g1(void)
{
printf("Inside g1: i = %d\n", i);
}
g2();
void g2(void)
{
printf("Inside g2: i = %d\n", i);
i = 20;
}
g1();
}
Inside f
Inside g2: i = 10
Inside g1: i = 20
The denition of function f contains the denitions of two other functions:
g1 and g2. That is, these two functions are nested within f. As such, they
can access any local identiers declared within their parent(s) except for those
having storage class register. The denition of a nested function need not
precede its use, but if it doesnt, it should at least be declared prior to use (as
is the case with g2 here).
Note that High C also permits declarations and statements to be interspersed within a block.
Nested functions require extra support if their address is to be taken and
used meaningfully. Since a nested function belongs to another function (and
can access its local variables), it has a current context which includes information about its parent. This context information is called an environment.
An environment along with the corresponding functions address is called a full
function value. So a pointer to a function is not able to store the context of a
nested function. Instead, a pointer to a nested function and a context pointer
are needed. High C refers to such a pointer pair collectively by the term full
function value variable, which can be declared as follows:
242
void pnf(void)!;
Here, pnf is declared as a variable capable of containing the full function
value of a nested function having no arguments and no return value. In short,
pnf is a pointer to a nested function that has these attributes. pnf can be
assigned the value of a nested function simply by assigning it the functions
name. The name of a nested function is not converted to a pointer to that
function. In fact, you cannot take the address of a nested function at all. pnf
can also be assigned the value of a non-nested function by assigning it the
dereferenced functions name. For example:
pnf = f1;
causes pnf to contain the value of the nested function f1, while:
pnf = *h;
causes pnf to contain the value of the non-nested function h. Since non-nested
functions do not have environments, a dummy one is created for them in contexts where one is needed (as in the second case above) but it is thereafter
ignored.
[As a side-issue, the most recent proposal from Bjarne Stroustrup to add a
readable alternative to trigraphs to ANSI C++ (and presumably to ISO C)
includes adding ! as a postx punctuator in declarations involving arrays. This,
of course, would lead to a conict with High C, as he has been made aware.]
The following example demonstrates the use of nested functions and full
function value variables.
#include <stdio.h>
main()
{
void f1(void)
{
puts("\nIn function f1");
}
void pnf(void)! = f1;
printf("sizeof(pnf) = %u\n", sizeof(pnf));
Miscellanea Jaeschke
243
void h(void);
pnf = *h; /* get value of global function */
pnf();
}
void g(void p(void)!)
{
puts("\nIn function g");
p();
/* call f1 */
}
void h(void)
{
puts("\nIn function h");
}
sizeof(pnf) = 8
In function f2
In function f1
In function f2
In function f1
In function g
In function f1
In function h
244
Note that while all function pointers in High C are 4 bytes long, the sizeof
a full function value is 8 bytes.
Full function values can be passed to functions. Function f2 expects such
an argument and is declared as follows:
void f2(void p(void)!);
This allows that function to be called using an expression of that type, as
follows:
/* use nested functions name */
f2(f1);
/* use variable having nested functions full value */
f2(pnf);
In fact, full function values can also be returned from functions using the
expected notation (even though it looks rather unusual). For example:
int g(void)(void)!;
Function g takes no arguments and returns a full function value of a function
taking no arguments and returning an int.
The last part of the example passed a nested functions full function value
to a global function which then indirectly calls that nested function. So while
a nested function is in some sense private to its parent(s), it can still be called
indirectly from outside its parent(s) but only if they have at least one activation
record active.
The operations permitted on full function values are intuitive and parallel
(for the most part) those permitted with function pointers. (You can even cast
one full function value to another.) However, the two mechanisms are mutually
exclusive. In summary, you can nd the full function value of a nested or nonnested function. You can take the address of a non-nested function but not
that of a nested function.
Another aspect, not demonstrated here, is that any label declared in a function is visible to all its subordinate nested functions. (Any labels of the same
spelling in nested functions temporarily hide the outer label.) According to the
High C documentation, Jumping to an ancestors label ... is a disciplined from
of Cs setjmp/longjmp and comes from Pascal.
Extended Number Syntax
Floating-point and integer constants may include underscore ( ) characters
among the digits. For example:
Miscellanea Jaeschke
245
#include <stdio.h>
main()
{
printf("%d, %f\n", 1_234_567, 9_876.123_456);
}
1234567, 9876.123456
Underscores may be used anywhere in the digit sequence, except at the
beginning so identiers such as 123 continue to be recognized correctly.
The pragma Keyword
High C supported the notion of pragmas before ANSI C invented the #pragma
directive. The syntax used was to have a pragma keyword instead. While
both the keyword and preprocessor directive are supported, the keyword form
is permitted inside macro denitions, thus solving one of the biggest objections
to pragma directives.
enum Representation
According to ANSI C (3.5.2.2 page 62, lines 4041), Each enumerated type
shall be compatible with an integer type; the choice of type is implementationdened. While most implementations make all enumerated types the same
size (that of an int) the standard permits dierent types to have dierent
representations so you can save on storage. Implementation-dened simply
means you must document how you do it.
To take advantage of this, it would seem reasonable to have a compiler
option or pragma to chose either smallest possible representation or int representation. Instead, High C gives semantics to short and long when applied
to enumeration types, as follows:
#include <stdio.h>
short
short
short
short
short
enum
enum
enum
enum
enum
t1
t2
t3
t4
t5
246
t1)
t2)
t3)
t4)
t5)
=
=
=
=
=
%u\n",sizeof(enum
%u\n",sizeof(enum
%u\n",sizeof(enum
%u\n",sizeof(enum
%u\n",sizeof(enum
t1));
t2));
t3));
t4));
t5));
}
sizeof(enum
sizeof(enum
sizeof(enum
sizeof(enum
sizeof(enum
t1)
t2)
t3)
t4)
t5)
=
=
=
=
=
1
2
2
4
4
The short modier directs the compiler to store the enumerated type object
in as small an integer object as possible. In the case of t1 all values t into a
char. Similarly, t2 will t into a short, t3 requires an unsigned short, and
t4 and t5 require an int.
Calendar of Events
March 45, 1991 Numerical C Extensions Group (NCEG) Meeting Location: Norwood, Mass. Analog Devices is the host. The fth
meeting will be held to consider proposals by the various subgroups. It
will now precede the X3J11 ANSI C meeting being held at the same
location (see below) and will run for two full days. For more information
about NCEG, contact the convenor Rex Jaeschke at (703) 860-0091 or
[email protected], or Tom MacDonald at (612) 683-5818 or [email protected].
Thinking Machines is hosting a reception on the Tuesday night at their
Cambridge oces where their (massively parallel) Connection Machine
will be demonstrated.
March 68, 1991 ANSI C X3J11 Meeting Location: Norwood, Mass.
Analog Devices is the host. This three day meeting will handle questions
from the public, interpretations, and other general business. Address
correspondence or enquiries to the vice chair, Tom Plum, at (609) 9273770 or uunet!plumhall!plum. Note that this meeting now follows NCEG
and is scheduled for three days instead of two.
March 1115, 1991 ANSI C++ X3J16 Meeting Location: Nashua,
New Hampshire.
May 1315, ISO C SC22/WG14 Meeting Location: Tokyo, Japan.
Contact the US International Rep. Rex Jaeschke at (703) 860-0091 or
[email protected] or the convenor P.J. Plauger at uunet!plauger!pjp for information.
Miscellanea Jaeschke
247
June 1314, 1991 First ISO C++ Meeting Location: Lund, Sweeden.
June 1721, 1991 ANSI C++ X3J16 Meeting Location: Lund, Sweeden.
June 2428, 1991 ACM SIGPLAN 91 Conference on Programming Language Design and Implementation Location: Toronto,
Canada. The conference seeks original papers relevant to practical issues
concerning the design, development, implementation, and use of programming languages.
August 1216, International Conference on Parallel Processing
Location: Pheasant Run resort in St. Charles, Illinois (near Chicago).
Submit software-oriented paper abstracts to Herbert D. Schwetman at
[email protected] or by fax at (512) 338-3600 or call him at (512) 338-3428.
August 2628, 1991 PLILP 91: Third International Symposium on
Programming Language Implementation and Logic Programming Location: Passau, Germany. The aim of the symposium is to
explore new declarative concepts, methods and techniques relevant for
implementation of all kinds of programming languages, whether algorithmic or declarative. Contact [email protected] for further information.
September 2325, 1991 Numerical C Extensions Group (NCEG)
Meeting Location: probably in the Washington D.C. area.
November, 1991 ANSI C++ X3J16 Meeting Location: Toronto,
Canada.
December 1113, Joint ISO C SC22/WG14 and X3J11 Meeting
Location: Tentatively in Milan, Italy.
248