Declarations And Program Shape

Once a Camp file grows past Hello, Camp., it becomes a set of declarations. That is the first real shift from “I can read a function body” to “I can read a Camp program.”

Declarations are the named pieces of a program: functions, structs, classes, interfaces, enums, newtypes, aliases, variables, constants, and members inside types. They are also where Camp puts most of the information that matters at a native boundary: visibility, exported API shape, allocation and error slots, receiver rules, generic capabilities, and interop markers.

This chapter is the map. Later chapters go deep on each feature; this one helps you look at a source file and understand what kind of thing each declaration is and why it belongs there.

Source Files Contain Declarations

A Camp source file is mostly a set of declarations. It may begin with file-level setup such as imports, build prelude directives, or a namespace, but ordinary program logic lives inside function and method bodies.

That means the top level of a file is not read as a sequence of statements to execute. It is read as the program surface: the functions, types, constants, aliases, and other named pieces that make up the module.

Here is a small complete file:

export int main()
{
	Size size = { .width = DEFAULT_WIDTH, .height = 3 };
	Console.writeLine(area(size));
	return 0;
}

int area(Size size)
{
	return size.width * size.height;
}

struct Size
{
	int width;
	int height;
}

inline int DEFAULT_WIDTH = 4;

Within the same source scope, top-level declaration order generally does not control visibility. Camp does not need the C habit of putting callees or types first just to satisfy forward declarations, so examples in this guide often use call order: show the entry point first, then show the declarations it calls or uses.

Order is still meaningful when the declaration kind gives it meaning. Field order affects layout. Enum value order affects default numbering. Interface member order affects the contract shape. Metadata and overload order should stay stable for readers and tools. When order matters, it should be deliberate.

Top-Level Declarations

Top-level declarations live directly in a source file or module. They introduce the names other declarations can use.

Common top-level forms:

FormIntroducesUse it when
functionA callable operation.You want named executable behavior.
structA value/layout type.Storage shape and field layout matter.
classAn identity/lifecycle type.Instances have identity, allocation, or virtual behavior.
interfaceA dynamic contract.Callers need a vtable-shaped API.
enumA named integer choice.A raw integer would hide meaning.
newtypeA distinct nominal wrapper.Existing representation needs a new meaning.
aliasAnother source name.You want readability without a new nominal type.
variableStorage.The program needs a named storage location.
inline constantA compile-time value.The value belongs in the API without storage.

The exact syntax varies by declaration kind, but the reading habit is the same: start with the modifiers, identify the kind of declaration, then read the name and body.

alias ByteCount = nuint;

enum FileKind
{
	REGULAR,
	DIRECTORY,
	DEVICE
}

inline ByteCount DEFAULT_BUFFER_SIZE = 4096;

ByteCount is only another name for nuint. FileKind is a nominal enum. DEFAULT_BUFFER_SIZE is a compile-time value that exported callers can see.

Functions

A function declaration has a name, a return type, parameters, and either a body or a valid bodyless surface.

int clampExitCode(int value)
{
	if (value < 0)
		return 1;
	if (value > 255)
		return 255;
	return value;
}

Read a function signature from left to right:

  1. Optional visibility says whether it crosses a broader source or ABI boundary.
  2. int is the result type.
  3. clampExitCode is the source name.
  4. (int value) is the parameter list.
  5. The body is the Camp implementation.

Parameters are part of the callable contract. Later chapters add more parameter forms, such as out, thrown, within, receivers, defaults, and generic capabilities. For now, treat the signature as the place where a function says what it needs from callers.

int parsePort(const char[] text, thrown ParseError error);
Buffer* createBuffer(nuint capacity, within allocator);

These are not just “an extra parameter or two.” A thrown slot changes error flow. A within parameter changes allocation policy. Camp puts those contracts in the declaration so a caller can see them at the call site.

Bodies And Semicolons

A body provides Camp implementation:

int doubleValue(int value)
{
	return value * 2;
}

A semicolon declares a surface without a Camp body only where that makes sense. Common examples include extern functions, interface entries, and abstract members:

@symbol("strlen")
extern nuint nativeStringLength(const char* text);

interface Reader
{
	nuint read(byte[] buffer);
}

Do not read every semicolon as a C-style prototype. In Camp, a bodyless declaration is still a real source contract. The kind of declaration tells you who is expected to provide the implementation: native code, an implementing class, a derived type, or another valid surface.

Type Declarations

Type declarations introduce names for values and objects.

struct Position
{
	int row;
	int column;
}

class Window
{
}

interface Writer
{
	void write(const char[] text);
}

The main type forms have different jobs:

Type formThink of it asTypical use
structDirect value storage.Coordinates, headers, spans, small records.
classAllocated identity with lifecycle.Handles, services, mutable objects.
interfaceDynamic dispatch contract.Plug-in points and abstraction boundaries.
enumNominal integer choice.Modes, states, status values, error kinds.
newtypeNominal wrapper around a representation.IDs, handles, typed callbacks.

This guide will return to all of them. The important early rule is that these forms are not interchangeable just because a target could represent them with similar bits. Camp uses the declaration kind as part of the source contract.

Members Inside Types

Types can contain fields and members. A member belongs to its containing type and participates in lookup, receiver binding, visibility, and generated API surface.

class Counter
{
	int value;

	void increment()
	{
		this.value += 1;
	}

	int getValue()
	{
		return this.value;
	}

	int computeTotal(const this)
	{
		return this.value + 10;
	}
}

Common member forms:

  • fields store data inside structs and classes;
  • methods operate on a receiver;
  • constructors initialize a new instance;
  • destructors clean up an instance;
  • static members belong to the type rather than one instance;
  • interface-marked methods fill a specific interface slot.

The receiver is the value or object a method is called on. In an instance method, this is the receiver. Get-style methods have an implicit const receiver, so getValue does not need to write const this. Other methods can write an explicit receiver when they want to state constness, lifetime, or extension-style behavior.

int computeTotal(const this)
{
	return this.value + 10;
}

That const this matters. It says the method can read through the receiver but cannot mutate it through that receiver view.

Visibility And Exported Shape

Visibility modifiers are part of the declaration’s contract.

ModifierMeaning in ordinary code
internalMake this declaration visible to other Camp source in the current project.
publicMake this declaration visible to statically linked Camp modules in the final artifact.
exportPut this declaration directly on the external API/ABI boundary.
no visibility keywordKeep it private to the relevant source scope.
externThe implementation or definition is provided outside Camp.

Use export deliberately. It is not just “public, but louder.” An exported declaration can affect generated headers, metadata, native symbols, layout commitments, and downstream compatibility. Most shared implementation helpers should be internal. Declarations intended to be shared with static project references or package modules, but not exposed outside the final artifact, should be public.

export struct PacketHeader
{
	uint magic;
	ushort version;
}

int checksum(PacketHeader header)
{
	return (int)(header.magic + header.version);
}

Here PacketHeader is external API. checksum is private unless it is later marked internal, public, or export.

A shared library can also export a public declaration through a separate projection declaration. Projection is useful when the internal Camp name should not be the external name, or when a type should expose only selected members:

namespace Pixel;

public class Container
{
	public int getValue() => 0;
	public void setValue(int value) { }
}

export Container { getValue as value, setValue as put_value } as px_container;

The source still uses Pixel::Container. The exported API view exposes the projected type name and projected member names.

Static Members And Inline Constants

Use static for behavior or storage associated with a type rather than an instance. Use inline for a compile-time value with no ordinary storage.

export int main()
{
	return ExitCodes.OK;
}

class ExitCodes
{
	inline int OK = 0;
	inline int USAGE = 2;
}

Inline constants are useful when a value belongs to an API contract:

export inline uint PROTOCOL_VERSION = 3;

Do not use const when you mean inline. const describes what can be mutated through a view. inline says the declaration is a compile-time value rather than storage.

Generic Declarations At A Glance

Functions and types can declare generic parameters. A generic parameter is not a blank check; its constraints say what operations the declaration can use.

export int main()
{
	int[] values = [1, 2, 3];
	return sumItems(values, valueOfInt) == 6 ? 0 : 1;
}

int sumItems<T: any>(T[] values, delegate int(in T item) valueOf, sizeof(T))
{
	int total = 0;
	foreach (auto item in values)
		total += valueOf(item);
	return total;
}

int valueOfInt(in int item) => item;

sumItems(values, valueOfInt) does not spell int at the call site. The compiler can often infer generic arguments from the values you pass. T: any allows the function to receive values of many element types, but it does not promise arithmetic, copying, construction, comparison, or formatting. The caller supplies valueOfInt to say how an item contributes to the sum. sizeof(T) is an explicit capability parameter that lets erased generic code know the element size.

Generics are a major feature, so this chapter only sets the expectation: generic declarations state the capabilities they need instead of assuming a large runtime can recover them later.

Choosing The Right Declaration Form

When you are deciding how to introduce a name, start with the promise you want the name to make.

NeedPrefer
Named behaviorFunction or method
Small record with visible layoutstruct
Object identity, allocation, or virtual behaviorclass
Dynamic contractinterface
Named set of integer valuesenum
Strong name for an existing representationnewtype
Readable synonym onlyalias
Stored mutable statevariable or field
Compile-time API valueinline constant
Native function or type from elsewhereextern declaration

The details will become sharper as the guide continues. For now, the key habit is simple: choose the declaration form that says what the name means, not just the one that happens to compile.