C, Ocean Edition I

Annex J. Lowering Notes

Annex
J
ISO C99 mapping
new
Status
Informative

J.0 Purpose

This annex records how the semantics of the edition are expected to be represented in an implementation, and it is written with Forge in mind. It is informative. An implementation may do anything that produces the specified behavior.

The notes exist because a specification that is easy to state and hard to keep is not much use. The organizing idea is that every guarantee of this edition should be structural in the intermediate representation rather than a rule that later passes must remember. See syntax.md section 55.


J.1 Normalize Early

Every semantic guarantee is established in translation phase 5, before any optimization. A guarantee expressed in the representation survives every later pass without any pass being taught about it.

Guarantee Representation
evaluation order operations emitted in order, with explicit temporaries
no array decay address computation emitted where the source wrote &a[0]
no function decay a function address is an explicit operation
wrapping signed arithmetic arithmetic operations carry wrapping semantics
traps an explicit check and trap edge at the operation
default initialization an explicit initializing store
restrict an explicit alias set on the pointer
volatile an ordered, non-removable memory operation
enumeration compatible type resolved to a concrete integer type at declaration
reproducibility every ordering derived from the program, never from an address

A back end that receives this representation cannot accidentally implement C99 semantics, because the C99 freedoms are not expressible in it.


J.2 Evaluation Order

For:

foo(a(), b(), c());

the frontend emits:

v0 = call a
v1 = call b
v2 = call c
call foo(v0, v1, v2)

No pass decides argument order, because no pass is given the opportunity. The question was answered by the language.

Reordering remains legal wherever it is unobservable. A scheduler may move v0 and v1 freely when neither call has effects and neither can trap. The representation states the order; the optimizer proves when the order does not matter.


J.3 Arrays and Functions

There is no generic decay node in the representation.

&values[0]

produces an explicit address computation from the array's base. An array-typed value flowing into a call is a type error caught in the frontend rather than a conversion inserted silently.

&compare

produces an explicit function address. A function symbol used as a value without an address operation is a type error.

The practical effect is that a reader of the intermediate representation can see every address the program forms, and a pass looking for address escapes finds them all in one node kind.


J.4 Arithmetic and Traps

Signed arithmetic carries wrapping semantics explicitly. The back end shall not select an instruction sequence or a target flag that makes overflow do something else, and shall not enable a mode in which the optimizer assumes overflow is absent.

A trapping operation lowers to a check and a trap edge:

t0 = icmp eq b, 0
br t0, trap_divzero, continue

continue:
t1 = sdiv a, b

Trap elision is ordinary optimization. Where range analysis proves b is nonzero, the check folds away and the trap edge disappears. Where it does not, the check stays, and its cost is the price of the guarantee. On most targets the checks that survive are a small fraction of arithmetic, because most divisors are constants or are already validated by the program.

The INT_MIN / -1 case needs its own check on targets where the hardware faults, and needs an explicit check on targets where it does not.

Shift counts lower to a range check plus the shift. Where the count is a constant, clause I.2.4 requires the frontend to have diagnosed an invalid one already, so no check survives.


J.5 Default Initialization

Default initialization emits a real store of zero. Ordinary dead-store elimination removes it wherever no read can observe it.

int value;

value = calculate();

return value;

The initializing store is dead on every path, and it disappears. The semantic model says the object begins valid; the generated code does only necessary work. See syntax.md section 29.

For a large aggregate, the initializing store lowers to a bulk zeroing operation, which the back end may turn into a memset-style call, a vectorized store, or nothing at all where the aggregate is fully overwritten before any read.

Two cases deserve attention from an implementer:


J.6 Aliasing

The representation carries no type-based alias information, because the language has none.

Alias facts come from three places, and each has a distinct representation:

The restrict alias set is the mechanism by which a programmer buys back the optimization that type-based aliasing used to provide by assumption. Making it explicit means an alias-sensitive transformation can point at the source line that authorized it.


J.7 volatile

A volatile access lowers to a memory operation marked as ordered and non-removable. Every pass shall treat it as having unknown effects with respect to other volatile operations, and shall preserve their relative order.

volatile places no ordering constraint on non-volatile operations, and carries no atomicity. An implementation shall not strengthen it, because a program that appears to work only because one implementation strengthened it will fail on another.


J.8 Two Front Ends, One Back End

Forge is expected to keep a faithful C99 front end alongside the Ocean Edition I front end.

They may share:

They shall not share a semantic model. The two languages converge only after each front end has resolved its own contract:

C99:
array expression
	context determines whether decay occurs
	result: pointer

Ocean Edition I:
array expression
	result: array

explicit &a[0]
	result: pointer
C99:
signed overflow
	undefined behavior

Ocean Edition I:
signed overflow
	wrapping result

Legacy semantics belong in the C99 front end. Edition semantics belong in the Ocean Edition I front end. The back end serves both, and neither front end is a mode of the other. See syntax.md sections 56 and 58.


J.9 Determinism of the Implementation

Annex K binds the implementation rather than the program, and it constrains the internals more than any other requirement in this collection. Four notes for an implementer.

The motivation to keep in mind is measurement rather than verification. A heuristic that consults an address makes a programmer's benchmark move when they rebuild without editing anything, which is the failure Annex K clause K.0.1 describes.

Identity, not address. Assign every entity a sequence number at the point it is created, and key containers on that. A compiler that iterates a pointer-keyed hash table and emits in that order will produce different symbol order on different runs, and the defect surfaces only when the allocator behaves differently. This is the single change that resolves most reproducibility failures.

Target arithmetic, not host arithmetic. Constant folding shall use the implementation's own routines for the target's types. Using host double to fold a target double produces different results when the host and target differ in format, in rounding, or in excess precision, and it silently ties the artifact to the machine that built it. See Annex K clause K.9.3 and clause 6.6.3.

Budgets in work, not in time. Where a pass gives up on a large function, count the units the pass controls, and make exhaustion produce a determined outcome: skip the transformation entirely, or fail. Keeping the partial work is what makes the result vary, because the amount completed is the machine-dependent quantity.

This costs less than it appears. A node budget bounds compile time just as a clock does, since the two correlate on any given machine. What is lost is the ability to spend a fixed number of seconds regardless of input, which was never a property worth having: it means a program's generated code depends on what else the machine was doing. Annex K clause K.2.1 supplies the pressure valve, which is that an implementation may always decline to translate.

Work the host anyway. Nothing in Annex K asks the implementation to be slow. Parallelize, cache across builds, take as much memory as the host will give, and use host vector instructions inside the compiler. All of it is permitted, because none of it can reach an artifact: the emitted bytes do not record how many cores computed them. Clause K.2.3 states the separation, and an implementer who reads Annex K as a performance constraint has read it wrongly.

The bootstrap is the best available test. An implementation that translates itself, translates itself again with the result, and compares the two artifacts exercises every one of these at once.


J.10 Cost Expectations

An implementer planning this work can expect the following, and should measure rather than assume.

Close to free.

Cheap in practice.

Potentially expensive, and worth measuring.

Design note. The edition's position is that a cost that is visible and measurable is preferable to a cost that is hidden in a class of defects. Where a guarantee proves too expensive for a real workload, the correct response is better analysis, and the incorrect response is a mode that removes the guarantee while still calling itself Ocean Edition I. See clause 4.8.1.