Expressions, Statements, And Operators Reference

This is the practical lookup chapter for Camp’s executable surface. It gathers the syntax you reach for while writing code: expressions, operators, calls, construction, cleanup, conditions, loops, foreach, try, catch, finally, await, postpone, and the smaller forms that are easy to forget until you need them.

This chapter is not a second tour of the language. Earlier chapters explain why the features exist. Here, the goal is quick, complete recall.

Expression Typing And Target Typing

Every expression has a static type after binding.

FormTyping rule
Literal with an inherent typeUses the literal’s type, then converts if allowed
defaultRequires a target type
nullRequires a pointer-like or callable-compatible target
String literalTargeted as string, wstring, astring, character pointer, counted text, or fixed character storage
Interpolated stringConstant text when all holes are constant text; otherwise a formatter value
Array literal [a, b]Requires or infers an array element type
Initializer list { ... }Requires a target aggregate, fixed array, optional, or other initializer-compatible type
LambdaRequires or infers a callable target
Omitted trailing out resultReconstructed only at an immediate binding or supported component selection
Method referenceHas the callable type or callable newtype implied by the referenced method
auto local initializerInfers one fixed static type
string title = "Report";
const char[] label = "Ready";
int? maybeCount = default;
auto values = [1, 2, 3];

auto does not make a local dynamic. The inferred type is fixed after the initializer is analyzed.

Literals

Literal formNotes
0, 42, 0xFFInteger literals are checked against the target type where needed
1.5Floating literal, converted according to the target type
true, falsebool
'x'Character literal, targetable to compatible character/code-unit types
"ready"String literal, target-driven
nullNull pointer/callable-like value where a compatible target exists
defaultDefault value for the target type
[1, 2, 3]Array literal
{ .x = 1, .y = 2 }Named initializer list
{ 1, 2, 3 }Positional initializer list

Initializer entries are either named or positional. Do not mix both in one initializer.

Point origin = { .x = 0, .y = 0 };
fixed byte[4] signature = [0x43, 0x41, 0x4D, 0x50];

Operator Precedence

From tightest to loosest:

LevelForms
Postfixcalls (), indexing [], slicing [..], member ., postfix ++, postfix --, generic postfix where applicable
Prefixunary +, unary -, !, ~, address-of &, dereference *, prefix ++, prefix --, await, postpone, casts
Multiplicative*, /, %
Additive+, -
Shift<<, >>
Relational<, <=, >, >=
Equality==, !=
Bitwise AND&
Bitwise XOR^
Bitwise OR|
Null coalescing??
Logical AND&&
Logical OR||
Conditional?:
Assignment=, +=, -=, *=, /=, %=, &=, |=, ^=, <<=, >>=

Use parentheses when mixing forms that are technically clear but visually busy:

bool selected = (flags & FLAG_SELECTED) != 0;
int scaled = (left + right) * factor;

The ^ token is also used in index and range contexts for from-end positions, as in values[^1] or values[2..^1]. That is slice/index syntax, not the bitwise XOR operator.

Arithmetic, Bitwise, And Boolean Operators

FamilyOperatorsResult
Arithmetic+, -, *, /, %Numeric result
Comparison<, <=, >, >=, ==, !=bool
Logical!, &&, ||bool
Bitwise~, &, |, ^, <<, >>Integral result
Conditionalcondition ? whenTrue : whenFalseCommon/target-compatible result
Null coalescingleft ?? rightCompatible non-null/optional result

+ also composes text when either side is a string, counted character view, interpolated string, or formatter:

Console.writeLine("total: " + total);
Console.writeLine($"left={left}, right={right}");

string message = ("total: " + total).copyString() finally delete;

Runtime text composition is a formatter. It does not allocate a primitive string implicitly, so assigning it to string requires explicit materialization:

string text = $"total: {total}";                                 // ERROR
string copy = ($"total: {total}").copyString() finally delete;   // OK

Constant interpolation and constant string concatenation remain ordinary text:

auto literal = $"ready";            // string
auto combined = "read" + "y";       // string

Textual + is left-associative:

Console.writeLine("Total: " + 1 + 2);    // Total: 12
Console.writeLine("Total: " + (1 + 2));  // Total: 3

+= is still numeric addition assignment. It does not append text or mutate a formatter.

Conditions must be bool. Integers, pointers, enums, and newtypes do not implicitly become truth values.

if (count != 0)
	process(values);

if (buffer != null)
	buffer.clear();
if (count)   // ERROR
	process(values);

Assignment And Update

The left side of an assignment must be writable.

FormMeaning
target = valueAssign converted value
target += valueAdd then assign for numeric values
target -= valueSubtract then assign
target *= valueMultiply then assign
target /= valueDivide then assign
target %= valueRemainder then assign
target &= valueBitwise AND then assign
target |= valueBitwise OR then assign
target ^= valueBitwise XOR then assign
target <<= valueShift left then assign
target >>= valueShift right then assign
target++, ++targetIncrement writable numeric location
target--, --targetDecrement writable numeric location
index++;
flags |= FLAG_VISIBLE;
items[position] = nextItem;

Writable locations include mutable locals, mutable fields, writable array elements, pointer targets, and setter-backed properties or indexers. const views, getter-only properties, and read-only receiver views are not writable.

Names And Member Access

SurfaceMeaning
nameResolve a local, parameter, member, type, namespace, generic parameter, or imported declaration
Namespace::NameQualified source name
value.memberField, method, property, expanded component, or static member access
Type.memberStatic member access
pointer.memberPointer receiver member access; Camp does not use ->
this.memberCurrent receiver member
base(...)Base constructor call in a class constructor
Std::Console.writeLine("ready");
window.resize(800, 600);
windowPointer.resize(800, 600);

Member access uses . for values and pointers. This is source syntax only; it does not erase the difference between a value and a pointer.

Compiler-expanded values expose components through member access:

nuint length = data.length;
byte* elements = data.elements;

if (maybeValue.specified)
	process(maybeValue.value);

auto callTarget = callback.call;
auto callContext = callback.context;

Property And Indexer Rewrites

Property syntax rewrites to eligible methods.

SurfaceRewritten form
value.Namevalue.getName()
value.Name = nextvalue.setName(next)
value.Item[index]value.getItem(index)
value.Item[index] = nextvalue.setItem(index, next)
value.[index]value.get(index)
value.[index] = nextvalue.set(index, next)
await value.Resultawait value.getResultAsync()

After rewriting, the expression is analyzed as the method call. thrown, await, generic arguments, visibility, lifetimes, and overload resolution all follow the method’s rules.

Iterator-returning getters are not property-like; call those methods explicitly.

Indexing, From-End Indexes, And Ranges

SurfaceMeaning
values[index]Element/indexer access
values[^1]From-end index, when the target has a visible length
values[start..end]Range boundary form
values[start..]Range from start to length
values[..end]Range from zero to end
values[..]Whole range
method(start, count)Direct index/count call
method(start..end)Boundary form for an @range pair

@index enables index-aware behavior such as from-end indexes. @range marks the first parameter in an index, count pair and enables range boundary syntax.

Direct comma form and boundary form are different:

text.slice(5, 2);   // index 5, count 2
text.slice(5..7);   // boundary 5 through boundary 7

Boundary form clamps boundaries before computing count. Direct form passes the explicit index, count values.

Calls And Arguments

Call formNotes
target()Ordinary call
target(a, b)Positional arguments
target(name: value)Named arguments
target(a, name: value)Positional and named arguments may be mixed
target(out value)Caller-provided success result slot
target(catch error)Caller-provided thrown/error slot
target(within allocator)Explicit allocator context argument
target<T>(value)Explicit generic argument
target(value)Generic arguments may be inferred when possible
target(sizeof(T))Explicit generic size capability
target(vtableof(T: I))Explicit generic interface-vtable capability

No ordinary parameter or expanded component may be supplied more than once. Default arguments are inserted from the static signature being called.

connect(host, port: 443, useTls: true);
file.read(buffer, out bytesRead, catch error);
copy = values.copyArray(within allocator);

Overload selector parameters participate in Camp’s intentionally limited overload resolution:

void write(CharWriter this, overload const char[] text);
void write(CharWriter this, overload int value);

writer.write("ready");
writer.write(42);

The overload keyword belongs to the declaration. The call site uses the ordinary argument expression, and the selector keeps the overload family small and ABI-visible.

Trailing out Result Binding

Trailing out parameters may be omitted only when the call result is immediately bound or selected in a supported result form.

void getBounds(out int width, out int height);

auto (width, height) = getBounds();

Equivalent explicit shape:

int width;
int height;
getBounds(out width, out height);

This is not a general tuple system. Use explicit out storage or a named struct when a result needs to be stored, returned, or passed around as one value.

Method References And Callable Invocation

A method name without a call refers to the method:

delegate void(const char[]) writeLine = Console.writeLine;
writeLine("ready");

Bound method references carry their receiver in the callable context:

auto formatter = date.format;
string text = formatter.copyString() finally delete;

Callable newtype ascription can give the reference a nominal callable type:

newtype delegate nuint StringFormatter(const this, char[] buffer = default);

struct Date
{
	nuint format(overload char[] buffer) : StringFormatter
	{
		// Format into buffer and return the required or written length.
	}
}

auto formatter = date.format; // StringFormatter

Callable values invoke with the same target(arguments) surface as functions.

Lambdas

Lambdas are callable expressions. They are target-typed by a delegate, once, iterator, async, or other compatible callable context.

delegate bool(in ImageInfo info) keepLarge = info => info.width >= 1024;

escaped delegate void(TimerHandle handle) tick = within(default) new delegate handle => {
	stopTimer(handle);
};

Captures are inferred from the lambda body and must satisfy the lifetime of the callable context. Escaped callables need escaped-safe captures.

Casts And Conversion Boundaries

Cast formUse
(T)valueOrdinary explicit conversion
(unsafe T)valueExplicit unsafe conversion across an unproven boundary
(escaped T)valueLifetime cast to escaped form where allowed
(scoped T)valueLifetime cast to scoped form where allowed
(unscoped(anchor) T)valueLifetime relation cast where allowed
(constof(anchor) T)valueDependent constness relation where allowed
FileHandle handle = (FileHandle)nativeHandle;
void* raw = (unsafe void*)callback.context;
escaped byte* retained = (escaped byte*)buffer;

Conversions do not tunnel through arrays, optionals, delegates, generic arguments, or callable signatures. Reconstruct the outer value when an inner component changes type.

Construction, Initialization, And Cleanup Expressions

FormMeaning
init Type(args)Construct in existing/local storage
new Type(args)Allocate storage, usually heap-like, then construct
within (allocator) expressionEvaluate allocation-aware expression in allocator context
within(default) expressionUse the default allocation path for the wrapped expression
delete valueDestroy/deallocate according to the operand and allocation path
expression finally cleanupRegister cleanup for the surrounding scope
init Type(args) { .field = value }Construct, then apply trailing initializer as one initialization operation
new Type(args) { .field = value }Allocate and construct, then apply trailing initializer
Buffer local = init Buffer(1024) finally delete;
Buffer* heap = within (allocator) new Buffer(1024) finally delete;

HttpRequest request = init HttpRequest("GET", url)
{
	.timeoutMs = 5000,
};

init creates the value in storage that is already present for the local, field, or destination. In ordinary local code, that usually means storage with the current block’s lifetime. new allocates storage before construction; in ordinary code that usually means heap-like storage through the current allocator path.

Special Expressions

FormMeaning
sizeof(T)Size capability or concrete type size
typenameof(T)Source-level type name as string
vtableof(T: Interface)Interface vtable capability for a concrete/generic type
symbolof(Name)Metadata-only symbol reference inside attribute arguments
await expressionAwait an async/callback-shaped operation
postpone callCapture a call for later invocation
throw valueProduce a value for the active thrown destination
thisCurrent receiver
classtypeLimited class-relative type form in allowed class member positions
defaultTarget-typed default value
nullTarget-typed null value

symbolof(...) is not a runtime expression. It is valid only inside metadata attribute arguments.

void writeType<T: any>(typenameof(T))
{
	Console.writeLine(typenameof(T));
}

void clear<T: any>(T[] values, sizeof(T));
void drawAll<T: implements Drawable>(T[] values, sizeof(T), vtableof(T: Drawable));

Blocks, Locals, And Fixed Storage

StatementNotes
{ ... }Block scope
;Empty statement
Type name;Local declaration
Type name = value;Local declaration with initializer
auto name = value;Inferred local declaration
fixed T[n] name;Inline fixed storage
expression;Expression statement
_ = expression;Intentional discard
{
	fixed byte[256] scratch;
	byte[] view = scratch;
	_ = fill(view);
}

Fixed storage owns inline storage. A span view over local fixed storage must not escape that storage’s lifetime.

_ is write-only. It may be assigned to, used as an out _ slot, or used as a catch _ slot, but it cannot be read or declared as an ordinary name.

Conditions

The controlling value must be bool in:

Form
if (condition)
while (condition)
do ... while (condition)
for (...; condition; ...)
condition ? left : right
left && right
left || right

Some statement forms allow declarations in the condition where the grammar permits them, but the final controlling value is still bool.

Control-Flow Statements

StatementNotes
if (condition) statementOptional else
while (condition) statementPre-test loop
do statement while (condition);Post-test loop
for (init; condition; next) statementInit may declare a local or evaluate an expression
switch (value) { ... }case and optional default labels
break;Exit nearest loop or switch
continue;Advance nearest loop
label:Label inside current function body
goto label;Jump to a visible label in the same function body
return;Return from void function or early exit where valid
return value;Return a value compatible with the function result
for (nuint index = 0; index < values.length; index++)
{
	if (values[index] == match)
		return index;
}

break, continue, return, and throw participate in ordinary cleanup flow. Pending finally cleanup for scopes being exited runs as structured control leaves.

goto is intentionally low-level. Unlike structured exits, it may jump out of blocks without running skipped finally work. Use structured control flow when cleanup and lifetime reasoning matter.

switch, case, And default

switch (mode)
{
	case READ:
		openReader();
		break;
	case WRITE:
		openWriter();
		break;
	default:
		throw E_INVALID_MODE;
}

break exits the switch. Prefer an explicit break, return, or throw at the end of each case unless fallthrough is deliberate and clear.

Enum values usually do not need qualification when the target enum type is known:

FileMode mode = OPEN_EXISTING;

Qualify when the target type is not known or when it improves clarity:

open(path, access, FileMode.OPEN_EXISTING);

foreach And yield

FormNotes
foreach (T value in source) statementIterate array or iterator-compatible source
foreach (auto value in source) statementInfer loop variable type
yield value;Produce one value from a generator body
foreach (auto item in values)
	total += item;

For arrays, iteration proceeds in index order. For iterators, foreach drives the iterator protocol and runs iterator cleanup on normal exit, break, return, and throw.

yield is valid only in a struct iter or class iter generator body. An iterator yields one ordinary value at a time; use a named struct when each item contains multiple fields.

Error Handling And Cleanup Statements

Statement/formNotes
try { ... } catch (E error) { ... }Handle compatible thrown value
try { ... } finally { ... }Cleanup on exit from protected block
catch (E error)Catch variable is an ordinary local in the catch body
finally statement;Register statement-level cleanup where allowed
expression finally cleanupRegister expression-level cleanup
throw value;Write compatible thrown value and exit current path
delete value;Destroy/deallocate operand
delete delegate;Delete generated context inside a valid new delegate lambda
try
{
	FileHandle file = FileHandle.open(path, READ, OPEN_EXISTING)
		finally close();
	process(file);
}
catch (IoError error)
{
	reportOpenFailure(error);
}

catch and out are both caller-provided storage patterns, but they are not the same. catch handles error flow. out is ordinary success data.

delete delegate is not a general delete operand. It is valid only inside a new delegate lambda, where it refers to the generated delegate context. In a void-returning lambda it may be the final statement; in a non-void lambda, use finally delete delegate as the final cleanup registration.

within Statements

within (allocator)
{
	string copy = text.copyString();
	Buffer* buffer = new Buffer(1024) finally delete;
}

A within statement establishes the allocation context for operations in the body that use allocator context. It does not extend the lifetime of existing values and does not make scoped data escaped.

Async And Deferred Call Forms

FormNotes
await callValid in async bodies; waits for awaitable async/callback result
postpone callCaptures a call and its supplied argument slots for later
@noawait async ...Async body contract: no suspension
@awaitwith parameterSelects the resumer for a suspending async body
int bytes = await BackgroundFiles.countBytesAsync(path, catch error);
auto later = postpone Console.writeLine("ready");

await is an expression operator. postpone captures a call shape; it is not a general closure literal.

Quick Statement Table

CategoryForms
Scopeblock, local declaration, fixed storage, empty statement
Selectionif, else, switch, case, default, ?:
Loopswhile, do, for, foreach
Loop exitsbreak, continue
Function exitsreturn, throw
Generator outputyield
Errorstry, catch, throw, catch arguments
Cleanupfinally, expression-level finally, delete
Allocation contextwithin statement, within expression, within argument
Low-level jumplabel, goto
Discard_

Quick Expression Table

CategoryForms
Valuesliterals, default, null, this
Namesunqualified names, :: qualified names
Access., [], property/indexer rewrites, expanded components
Callsordinary calls, named arguments, defaults, out, catch, within
Constructioninit, new, initializer lists, trailing initializers
Conversioncasts, unsafe casts, lifetime casts
Operatorsarithmetic, comparison, logical, bitwise, assignment, update
Capabilitiessizeof, typenameof, vtableof
Metadata-onlysymbolof
Async/deferredawait, postpone
Error flowthrow