C* Extensions
The previous page described the syntax C* inherits unchanged from HOL Light. This page lists C*‘s extensions: type constants for expressing separation logic and C types, the constants and notations built from them, and naming conventions that connect logical variables to the C program being verified.
The constructs listed here appear inside backtick-quoted logical expressions. The entries describe syntax and types: each name, its type, and how to read it. The meaning of the separation-logic constants is given in Memory Model and Assertions, and the memory predicates are developed in Typed Memory Predicates.
C* introduces four type constants into HOL Light’s type language. They follow the type syntax described on the previous page and may appear wherever a type is allowed, including annotations such as (x:addr)(x:addr) and postfix constructor applications such as (hprop)list(hprop)list.
| Type | Form | Meaning |
|---|---|---|
hprophprop |
0-ary | heap proposition — the type of separation-logic assertions |
ctypectype |
inductive | the logic-level representation of a C type |
struct_namestruct_name |
0-ary | the logic-level representation of a struct name |
fieldfield |
0-ary | the logic-level representation of a field name |
struct_namestruct_name and fieldfield are declared as bare 0-ary type constants without constructors. The library predeclares no program-specific struct-name or field-name constants; a program introduces its own, as described under “Structs and Fields” below. ctypectype is an inductive type; its constructors are listed in the section on C types. hprophprop is the type of assertions; in C*‘s shallow embedding it unfolds to a predicate over memory, but at the level of syntax it behaves as an opaque type constant.
Two type abbreviations name the roles that integers play in the memory model:
addr = :intbyte = :int
addr = :intbyte = :int
Type abbreviations are transparent: addraddr and intint are the same type, and the type checker does not distinguish them. The abbreviations document the arguments’ roles: the signature addr->ctype->int->hpropaddr->ctype->int->hprop distinguishes the address from the stored value, whereas int->ctype->int->hpropint->ctype->int->hprop does not.
An unsuffixed numeral is a natural number; C* extends the lexer so that a numeral with the suffix ii is an integer.
| Literal | Type | Note |
|---|---|---|
123123 |
:num:num |
natural-number literal, HOL Light’s default |
123i123i |
:int:int |
integer literal, C*‘s extension |
0x1f0x1f, 0b10110b1011 |
:num:num |
hexadecimal and binary, also accepting the ii suffix |
C* models addresses, stored values, sizes, and offsets as integers of type intint, so nearly every numeral in a C* assertion carries the ii suffix. Dropping it is the most common type error when writing assertions: data_at p Tint 42data_at p Tint 42 fails to type-check, because data_atdata_at expects an intint and 4242 is a numnum.
Negation of a literal is the prefix ----, not --: the additive inverse of 1i1i is --(1i)--(1i), since -- is reserved for binary subtraction.
The constants below form the separation-logic vocabulary. The first group builds assertions and has result type hprophprop; the last group states relations between assertions and has result type boolbool. Thus, p |-- qp |-- q is a proposition about two assertions, not an assertion itself.
| Notation | Type | Reads as |
|---|---|---|
empemp |
hprophprop |
the heap is empty |
truetrue |
hprophprop |
any heap at all (heap truth) |
falsefalse |
hprophprop |
no heap at all (heap falsehood) |
purepure |
bool->hpropbool->hprop |
the proposition holds; no constraint on the heap |
factfact |
bool->hpropbool->hprop |
the proposition holds and the heap is empty |
**** |
hprop->hprop->hprophprop->hprop->hprop |
separating conjunction |
&&&& |
hprop->hprop->hprophprop->hprop->hprop |
heap conjunction |
|||| |
hprop->hprop->hprophprop->hprop->hprop |
heap disjunction |
-*-* |
hprop->hprop->hprophprop->hprop->hprop |
magic wand (separating implication) |
-->--> |
hprop->hprop->hprophprop->hprop->hprop |
heap implication |
existsexists |
(A->hprop)->hprop(A->hprop)->hprop |
existential quantifier over assertions |
forallforall |
(A->hprop)->hprop(A->hprop)->hprop |
universal quantifier over assertions |
|--|-- |
hprop->hprop->boolhprop->hprop->bool |
entailment |
-|--|- |
hprop->hprop->boolhprop->hprop->bool |
assertion equality |
-||--||- |
hprop->hprop->boolhprop->hprop->bool |
assertion equivalence (mutual entailment) |
existsexists and forallforall are binders: they are written exists x. pexists x. p and forall x y. pforall x y. p , as syntactic sugar for applying the constant to a lambda abstraction. The type variable AA in their signatures is unconstrained: one may quantify over an intint, an addraddr, a list, or any other type.
Six of these names are overloaded: in addition to their hprophprop versions, truetrue, falsefalse, existsexists, forallforall, &&&&, and |||| each have a Boolean counterpart. Type inference selects the appropriate one from context. In fact(x > 0i && y > 0i)fact(x > 0i && y > 0i) the &&&& is Boolean conjunction (HOL Light’s /\/\); in fact(x > 0i) && empfact(x > 0i) && emp it is heap conjunction. By contrast |--|--, -|--|-, -||--||-, -*-*, -->-->, ****, empemp, purepure, and factfact are C*-specific names with no Boolean interpretation. C* conventionally uses different notation for the two quantifier layers: the boolbool layer uses the symbols !! and ??, the hprophprop layer the words forallforall and existsexists.
The three relational notations must be distinguished. -|--|- is HOL Light’s equality at type hprophprop, that is (=):hprop->hprop->bool(=):hprop->hprop->bool under another name; theorem statements about assertions are printed with this notation. Thus, a representation predicate’s definition is written with == but printed with -|--|-. -||--||- is a separately defined constant, logic_equivlogic_equiv, meaning entailment in both directions. The two agree extensionally, but they are distinct terms, and a rule stated with one does not match a goal written with the other; the library’s rules are stated with -|--|-.
For the precedence and associativity of all these infixes see the operator table in HOL Light Syntax for their definitions and the laws they satisfy see Memory Model and Assertions.
These constants form assertions that describe C memory. Each takes an address, a ctypectype specifying how to read the memory, and a description of its contents.
| Signature | Meaning |
|---|---|
data_at : addr->ctype->int->hpropdata_at : addr->ctype->int->hprop |
the address holds the given value, stored at the given C type |
undef_data_at : addr->ctype->hpropundef_data_at : addr->ctype->hprop |
the address holds an allocated but uninitialized location of the given C type |
array_at : addr->ctype->int->(int)list->hproparray_at : addr->ctype->int->(int)list->hprop |
nn consecutive elements starting at the address, with the list specifying their contents |
array_at_rec : addr->ctype->int->int->(int)list->hproparray_at_rec : addr->ctype->int->int->(int)list->hprop |
the half-open index segment lolo–hihi of an array, with the list as its contents |
array_at_missing_i_rec : addr->ctype->int->int->int->(int)list->hproparray_at_missing_i_rec : addr->ctype->int->int->int->(int)list->hprop |
the same segment with the element at one index split out |
undef_array_at : addr->ctype->int->hpropundef_array_at : addr->ctype->int->hprop |
nn consecutive uninitialized elements starting at the address |
undef_array_at_rec : addr->ctype->int->int->int->hpropundef_array_at_rec : addr->ctype->int->int->int->hprop |
an uninitialized half-open index segment |
undef_array_at_missing_i_rec : addr->ctype->int->int->int->int->hpropundef_array_at_missing_i_rec : addr->ctype->int->int->int->int->hprop |
an uninitialized segment with one index removed |
Argument order for the segment forms is: address, C type, then the indices, then (for the initialized family only) the contents list. The missing_imissing_i forms take the removed index first among the indices, as in array_at_missing_i_rec x ty i lo hi larray_at_missing_i_rec x ty i lo hi l. The undef_undef_ family carries no contents list, since there is nothing to say about the value of uninitialized memory; the index arguments are otherwise parallel, with a trailing length argument in place of the list.
Element ii of an array based at xx is at address x + i * sizeof(ty)x + i * sizeof(ty), which is exactly the address C* produces when it elaborates the C subscript expression a[i]a[i]. For the rules for splitting, merging, and rebasing these predicates, see Typed Memory Predicates.
A value of type ctypectype represents a C type in the logic: it tells data_atdata_at how many bytes a location occupies and which values it may hold. Nine constructors cover the scalar types C* models. Compound C types also have constructors; the logic interprets TaliasTalias through its underlying type and treats the others as opaque.
| Constant | C type | Constant | C type |
|---|---|---|---|
TcharTchar |
charchar |
TucharTuchar |
unsigned charunsigned char |
TshortTshort |
shortshort |
TushortTushort |
unsigned shortunsigned short |
TintTint |
intint |
TuintTuint |
unsigned intunsigned int |
Tint64Tint64 |
64-bit signed | Tuint64Tuint64 |
64-bit unsigned |
TptrTptr |
any pointer type |
TptrTptr represents every pointer type. The logic does not record the type of the pointed-to object, so int *int * and struct list *struct list * are both TptrTptr.
The compound C types are represented but not modelled:
Tstruct string | Tenum string | Tunion string |Talias string ctype | Tarray int ctype |Tfun (string)list (ctype)list ctype
Tstruct string | Tenum string | Tunion string |Talias string ctype | Tarray int ctype |Tfun (string)list (ctype)list ctype
TstructTstruct, TenumTenum and TunionTunion carry only the tag name; Talias name tyTalias name ty is a typedeftypedef, and the functions below use its underlying type; Tarray n tyTarray n ty is an array of nn elements; TfunTfun records a function signature (parameter names, parameter types, result type) and occurs only inside the function-pointer specifications fnspecfnspec and fnspec_wfnspec_w described on Typed Memory Predicates. Hand-written assertions use the nine scalar constants and access struct memory through field_addrfield_addr.
Three functions extract information from a ctypectype:
| Signature | Meaning |
|---|---|
sizeof : ctype->intsizeof : ctype->int |
size in bytes: 1i1i for the char types, 2i2i for the short types, 4i4i for the intint types, 8i8i for the 64-bit types, for TptrTptr and for TfunTfun; uses the underlying type for TaliasTalias; no clause for TstructTstruct, TenumTenum, TunionTunion, TarrayTarray |
min_of : ctype->intmin_of : ctype->int |
smallest representable value (&0&0 for every unsigned type); uses the underlying type for TaliasTalias |
max_of : ctype->intmax_of : ctype->int |
largest representable value; uses the underlying type for TaliasTalias |
Struct and field names are represented in the logic by constants of the dedicated types struct_namestruct_name and fieldfield. The library predeclares no program-specific name constants of either type; a program introduces the ones it needs, typically with new_constnew_const in a global proof declaration. The naming convention is that a struct-name constant is the C struct name prefixed with TT, and a field-name constant is the C field name prefixed with FF — struct liststruct list becomes TlistTlist, and its fields headhead and tailtail become FheadFhead and FtailFtail. C* uses these names to match the C program with its logical representation, so the prefixes are required.
Field addresses are computed by two constants:
field_offset : struct_name->field->intfield_addr : addr->struct_name->field->addr
field_offset : struct_name->field->intfield_addr : addr->struct_name->field->addr
field_offsetfield_offset is a primitive constant declared without a definition: the byte offsets for a struct are left uninterpreted and supplied to the verifier for each program. field_addrfield_addr has a definition whose theorem is registered in the theorem database as field_addr_propfield_addr_prop:
field_addr x s f = x + field_offset s f
field_addr x s f = x + field_offset s f
So field_addr pt Tlist Ftailfield_addr pt Tlist Ftail denotes the address of the tailtail field of the struct liststruct list at ptpt, and nested accesses compose: the C expression &it->node.next&it->node.next becomes field_addr (field_addr it Titem Fnode) Tlink Fnextfield_addr (field_addr it Titem Fnode) Tlink Fnext.
C’s bitwise operators are not primitive in the logic. C* defines four families over intint, for signed and unsigned integers at 32 and 64 bits, using the machine-word semantics of HOL Light’s word library.
| Signed | Unsigned | Type and reading |
|---|---|---|
i32_andi32_and, i64_andi64_and |
u32_andu32_and, u64_andu64_and |
int->int->intint->int->int, bitwise and |
i32_ori32_or, i64_ori64_or |
u32_oru32_or, u64_oru64_or |
int->int->intint->int->int, bitwise or |
i32_xori32_xor, i64_xori64_xor |
u32_xoru32_xor, u64_xoru64_xor |
int->int->intint->int->int, bitwise exclusive or |
i32_noti32_not, i64_noti64_not |
u32_notu32_not, u64_notu64_not |
int->intint->int, bitwise complement |
i32_shli32_shl, i64_shli64_shl |
u32_shlu32_shl, u64_shlu64_shl |
int->int->intint->int->int, left shift |
i32_shri32_shr, i64_shri64_shr |
u32_shru32_shr, u64_shru64_shr |
int->int->intint->int->int, right shift |
Each operator is an ordinary HOL definition, not an axiom. Its defining theorem is available from the theorem database under the name <op>_def<op>_def. The definitions convert value operands to N-bit words with iwordiword: both operands for bitwise and, or, and exclusive or, the single operand for complement, and the value being shifted for shifts. Shift counts are converted with num_of_intnum_of_int. After applying the word operation, the result is converted back to an integer using ivalival (signed interpretation) for the i32_i32_/i64_i64_ family and &(val ...)&(val ...) (unsigned interpretation) for the u32_u32_/u64_u64_ family.
i32_and x y = ival (word_and ((iword x):(32)word) ((iword y):(32)word))u32_and x y = &(val (word_and ((iword x):(32)word) ((iword y):(32)word)))i64_shr x y = ival (word_ishr ((iword x):(64)word) (num_of_int y))u64_shr x y = &(val (word_ushr ((iword x):(64)word) (num_of_int y)))
i32_and x y = ival (word_and ((iword x):(32)word) ((iword y):(32)word))u32_and x y = &(val (word_and ((iword x):(32)word) ((iword y):(32)word)))i64_shr x y = ival (word_ishr ((iword x):(64)word) (num_of_int y))u64_shr x y = &(val (word_ushr ((iword x):(64)word) (num_of_int y)))
Two details of this encoding matter when reasoning about these operators.
- Value operands wrap for every integer.
iwordiwordis defined on every integer, including negative integers, sou32_and (a - b) cu32_and (a - b) cis well specified even whenb > ab > a. The model keeps intermediate results of unsigned C arithmetic as mathematical integers and has nou32_subu32_sub; a partial encoding would leave such terms unsimplifiable. - Negative shift counts are unspecified. The second operand of
shlshlandshrshrpasses throughnum_of_intnum_of_int, which is unspecified on negative inputs. This is consistent with C’s undefined behaviour for negative shift counts. A proof that evaluates a shift must establish that the count is non-negative.
Right shift also depends on signedness at the word level: the signed family uses the arithmetic shift word_ishrword_ishr, the unsigned family the logical shift word_ushrword_ushr.
Two further operators model C’s conversion of a value to a given bit width:
signed_last_nbits x y = cast_signed y xunsigned_last_nbits x y = cast_unsigned y x
signed_last_nbits x y = cast_signed y xunsigned_last_nbits x y = cast_unsigned y x
Both have type int->int->intint->int->int, where xx is the value and yy the target width in bits. The argument order is the reverse of that used by cast_signedcast_signed and cast_unsignedcast_unsigned in the C-semantics layer. cast_unsigned n zcast_unsigned n z is z rem exp_2 nz rem exp_2 n (HOL’s remrem is Euclidean, so the result is never negative), and cast_signedcast_signed adjusts the residue to lie in the signed range. Both are total and work across bit widths: they handle negative xx and widths of 64 bits and beyond.
The following three facts have been proved and are available from the theorem database:
| Theorem | Statement |
|---|---|
unsigned_last_nbits_rangeunsigned_last_nbits_range |
!x y. &0 <= unsigned_last_nbits x y /\ unsigned_last_nbits x y <= max_unsigned y!x y. &0 <= unsigned_last_nbits x y /\ unsigned_last_nbits x y <= max_unsigned y |
unsigned_last_nbits_idunsigned_last_nbits_id |
!x y. &0 <= x /\ x < exp_2 y ==> unsigned_last_nbits x y = x!x y. &0 <= x /\ x < exp_2 y ==> unsigned_last_nbits x y = x |
signed_last_nbits_rangesigned_last_nbits_range |
!x y. &0 < y ==> min_signed y <= signed_last_nbits x y /\ signed_last_nbits x y <= max_signed y!x y. &0 < y ==> min_signed y <= signed_last_nbits x y /\ signed_last_nbits x y <= max_signed y |
The _range_range lemmas give the range of the result and can discharge a data_atdata_at range obligation after a conversion. unsigned_last_nbits_idunsigned_last_nbits_id states that conversion preserves a value already in range. Its premise is exactly the condition under which the C* frontend removes a conversion from a verification condition. The definitions of cast_signedcast_signed and cast_unsignedcast_unsigned are themselves exported, under the names cast_signed_defcast_signed_def and cast_unsigned_defcast_unsigned_def, so a proof block can unfold these definitions when needed.
C*‘s memory model includes the store in the heap: an assertion has no notion of “program variable”, so a C variable must appear in the logic as an ordinary logical variable. The symbolic execution engine generates these variables using a fixed naming scheme, and assertions in contracts and invariants are written using exactly these names.
| Logical variable | Type | Denotes |
|---|---|---|
x__addrx__addr |
addraddr |
the address of the program variable xx — parameter, local, or global |
x__prex__pre |
the value type of xx |
the value of the parameter xx on entry to the function |
__return__return |
the return type | the function’s return value, usable in a postcondition |
The separator is a double underscore. The lexer treats these names as ordinary identifiers: __ is an alphanumeric character in HOL Light, so x__addrx__addr is a single token. A program can still declare its own variable with that name, causing a naming collision.
An intint parameter xx therefore contributes the resource data_at x__addr Tint x__predata_at x__addr Tint x__pre to the symbolic state at function entry, and an uninitialized local int rint r contributes undef_data_at r__addr Tintundef_data_at r__addr Tint. The value range of x__prex__pre is part of the data_atdata_at resource, rather than a separate pure fact in the symbolic state. A proof can extract it using the range rule.
For int abs_diff(int a, int b)int abs_diff(int a, int b), “the result is the absolute difference of the two arguments” is stated over a__prea__pre, b__preb__pre, and __return__return:
int abs_diff(int a, int b) [[cst::require(`emp`)]] [[cst::ensure(`fact(__return == a__pre - b__pre || __return == b__pre - a__pre) ** fact(__return >= 0i)`)]];int abs_diff(int a, int b) [[cst::require(`emp`)]] [[cst::ensure(`fact(__return == a__pre - b__pre || __return == b__pre - a__pre) ** fact(__return >= 0i)`)]];The parameters’ own storage — data_at a__addr Tint a__predata_at a__addr Tint a__pre and data_at b__addr Tint b__predata_at b__addr Tint b__pre — need not be mentioned: symbolic execution adds it to the state on entry and discards it on exit, so a contract describes only memory the caller owns.
For contracts and invariants using these names, see Function Contracts, Loop Invariants, and Assertions for their use in the conditions generated by the engine, see Verification Condition Generation.
Logical expressions enter a C* program through C23 attributes in the cst::cst:: namespace. Function attributes follow the parameter list of a prototype or definition; statement attributes precede the statement they govern (a loop for the invariants, a call for wherewhere) or stand as a statement of their own. Every argument in the table below is a backtick quotation, parsed according to the rules in this group of reference pages.
| Attribute | Meaning |
|---|---|
[[cst::param(`x:int`, …)]][[cst::param(`x:int`, …)]] |
ghost parameters of a function; leading :A:A entries are type parameters |
[[cst::require(`p`)]][[cst::require(`p`)]] |
precondition |
[[cst::ensure(`q`)]][[cst::ensure(`q`)]] |
postcondition; __return__return names the result |
[[cst::invariant(`p`)]][[cst::invariant(`p`)]] |
loop invariant, on a whilewhile, forfor or dodo-whilewhile loop; must describe the complete symbolic state at the loop head |
[[cst::partial_invariant(`p`)]][[cst::partial_invariant(`p`)]] |
loop invariant naming part of the state; the rest is framed through unchanged |
[[cst::assert(`p`)]][[cst::assert(`p`)]] |
inline assertion; asserts and replaces the whole symbolic state at that point |
[[cst::partial_assert(`p`)]][[cst::partial_assert(`p`)]] |
inline assertion checked against the state; the rest of the state is preserved |
[[cst::where(`v`, `e`)]][[cst::where(`v`, `e`)]], [[cst::where_type(`:T`, `:ty`)]][[cst::where_type(`:T`, `:ty`)]] |
one explicit ghost argument (value or type) for the call in the following statement |
[[cst::given(`k:int`, …)]];[[cst::given(`k:int`, …)]]; |
a standalone statement ending in ;;: binds the named existential variables of the current assertion so that a following wherewhere or an inner loop’s invariant can mention them; proof blocks do not need it |
[[cst::proof]] { … }[[cst::proof]] { … }, [[cst::proof]] decl[[cst::proof]] decl |
a proof block, or a ghost declaration: a typedef, variable or function that exists only for the proof |
Three further spellings are produced by the toolchain rather than written by hand: [[cst::include("cstar.h")]];[[cst::include("cstar.h")]]; is what a #cst_include#cst_include directive becomes after preprocessing, [[cst::module("dep.c")]];[[cst::module("dep.c")]]; is what a #require "dep.c"#require "dep.c" module dependency becomes (the name avoids a clash with the precondition attribute), and [[cst::lsp]][[cst::lsp]] is the marker the editor integration injects to dump the symbolic state at a line. The tutorial pages Function Contracts, Loop Invariants, and Assertions and Your First C* Program show these attributes in use.
- C* adds the type constants
hprophprop,ctypectype,struct_namestruct_name,fieldfieldto HOL Light’s type language, plus the transparent abbreviationsaddr = :intaddr = :intandbyte = :intbyte = :int. - A numeral is a
numnumunless it carries theiisuffix, which makes it anintint; theorem statements from the library use the equivalent&n&ninjection notation instead. - The heap-proposition constants —
****,&&&&,||||,-*-*,-->-->,empemp,purepure,factfact,truetrue,falsefalse,existsexists,forallforall— build assertions;|--|--,-|--|-,-||--||-relate assertions and have result typeboolbool. Six of the names are overloaded with Boolean counterparts. data_atdata_at/undef_data_atundef_data_atand thearray_atarray_at/undef_array_atundef_array_atfamilies describe C memory; every signature takes actypectypeargument, and array elementiiis atx + i * sizeof(ty)x + i * sizeof(ty).- Nine
ctypectypeconstructors cover the scalar C types andTaliasTaliasis interpreted through its underlying type;TstructTstruct,TenumTenum,TunionTunion,TarrayTarrayandTfunTfunare opaque;sizeofsizeof,min_ofmin_of,max_ofmax_ofextract information from the scalar types; structs enter throughTT-prefixed andFF-prefixed constants together withfield_addr x s f = x + field_offset s ffield_addr x s f = x + field_offset s f. - The bitwise operators are defined over HOL’s machine words: value operands wrap for every integer via
iwordiword, and shift counts pass throughnum_of_intnum_of_int(a negative count being C undefined behaviour). Width conversions are defined using integer remainder, and the range lemmas forsigned_last_nbitssigned_last_nbitsandunsigned_last_nbitsunsigned_last_nbitsare proved. Neither family is axiomatized. - Program variables reach the logic as
x__addrx__addr,x__prex__pre, and__return__return. - Quotations enter a program through the
cst::cst::attributes:paramparam,requirerequire,ensureensureon functions;invariantinvariant,partial_invariantpartial_invariant,assertassert,partial_assertpartial_assert,wherewhere,where_typewhere_type,givengivenon statements;proofprooffor proof blocks and ghost declarations.