Neutral source types and markers
@tsonic/core owns target-neutral source semantics. These are compiler-owned
virtual modules; users do not install an @tsonic/core npm package directly.
@tsonic/core/types.js
Primitive aliases
| Export | Source meaning | TypeScript carrier |
|---|---|---|
bool | Boolean | boolean |
char | Unsigned 16-bit character/code unit | string |
int8, int16, int32 | Signed fixed-width integers | number |
uint8, uint16, uint32 | Unsigned fixed-width integers | number |
int64, int128 | Signed fixed-width integers | bigint |
uint64, uint128 | Unsigned fixed-width integers | bigint |
nativeInt, nativeUint | Target-native signed/unsigned integers | number |
float16, float32, float64 | IEEE-style floating-point domains | number |
decimal | Target decimal domain | number |
The TypeScript carrier controls checking syntax; the retained primitive fact controls target meaning. Targets reject unsupported exact primitives rather than silently widening them.
Type markers
| Export | Meaning |
|---|---|
Pointer<T> | Typed mutable storage location |
RawPointer | Untyped address carrier; not an arbitrary object’s identity |
FunctionPointer<TArgs, TReturn> | Exact native function-pointer signature |
FixedArray<T, N> | Fixed-length array; N must be one exact non-negative safe numeric or bigint literal type |
NativePointer<T> | Target-native typed pointer used by explicit native-pointer operations |
DataLayout | Provider-selected ABI identity and immutable descriptor |
MemoryLayout<T> | Exact size, alignment, stride and selected physical field or array-element layouts for T |
MemoryFieldLayout<T> | One selected field’s offset and alignment |
Pointer<T> and NativePointer<T> are different contracts. The first is a
safe closed location abstraction. The second requests the target’s native
pointer representation and safety rules.
FixedArray<T, 2> has a number-based readonly .length of type 2;
FixedArray<T, 2n> has a bigint-based readonly .length of type 2n. Shared
metadata stores both counts exactly as bigint, without changing that source
meaning. Wider number/bigint types, unions, negative or fractional extents,
and unsafe numeric literals are rejected. Use an exact bigint literal such as
9007199254740993n beyond the safe-number range. Exact source evidence does not
guarantee a native value representation or an implemented .length operation;
see the C# and
Rust limits.
@tsonic/core/lang.js
Argument and ownership markers
| Export | Meaning |
|---|---|
writeOnlyRef(value) | Selected argument is writable but not read |
readWriteRef(value) | Selected argument is read and written |
readOnlyRef(value) | Selected argument is passed by readonly reference |
sharedBorrow(value) | Shared-borrow flow intent |
mutableBorrow(value) | Exclusive mutable-borrow flow intent |
move(value) | Ownership-transfer flow intent |
These markers do not manufacture target semantics. The selected signature and target policy must independently support the requested mode.
Structure and metadata markers
| Export | Meaning |
|---|---|
struct(shape) | Declares an exact value-type shape from proven field<T>() members |
field<T>() | Declares a field with explicit source type evidence |
attribute<T>(...args) | Starts an exact attribute-application builder |
defaultValue<T>() | Requests the target default for exact T |
Example:
import { field, struct } from "@tsonic/core/lang.js";
import type { int32 } from "@tsonic/core/types.js";
export const Point = struct({
x: field<int32>(),
y: field<int32>(),
});
Attribute placement uses exact selectors:
attribute<Controller>()
.method((controller) => controller.handle)
.parameter("request")
.target("param")
.add(RouteAttribute, "/items");
The selected provider owns the target attribute identity and legal values.
Compile-time intent
| Export | Meaning |
|---|---|
comptime(expression) | Require target compile-time evaluation of the exact expression |
comptime<T>() | Project an exact selected compile-time parameter or literal type into value position |
comptimeIf(condition) | Require compile-time selection of the directly enclosing if or conditional expression |
unroll(iterable) | Require compile-time expansion of the directly enclosing for...of loop |
import { comptime, comptimeIf, unroll } from "@tsonic/core/lang.js";
const enabled = comptime(true);
if (comptimeIf(enabled)) {
for (const value of unroll([1, 2, 3])) {
consume(value);
}
}
These calls record target-neutral intent and exact selected source evidence;
source-core does not evaluate the expression or choose target syntax. Targets
must prove that the selected operation is representable in their compile-time
domain. An ordinary runtime type is not automatically a valid comptime<T>()
parameter. Compile-time intent does not imply copying, ownership transfer,
runtime materialization, or target-independent support for arbitrary evaluation.
Import aliases, namespace imports, and parentheses preserve intrinsic identity. Same-spelled local functions do not become intrinsics. Core intrinsics must be imported directly from their owning virtual module, not re-exported through a local barrel.
const decision = comptimeIf(true) and const values = unroll([1, 2]) are
invalid placements. So are if (comptimeIf(true) && flag) and
for (const key in unroll(object)): the marker must select the exact owning
condition or for-of iterable, with parentheses permitted.
Targets read tsonicCompileTimeFactKey through the public
@tsonic/source-core/facts entrypoint. Its value, type, condition, and
iteration variants retain the exact operand/type and result-type identities;
targets must not rediscover the request from the callee’s spelling.
Typed-location operations
| Export | Meaning |
|---|---|
addressOf(storage) | Address an existing proven storage location |
allocatePointer(initial) | Allocate independent typed storage |
loadPointer(pointer) | Read a typed location |
storePointer(pointer, value) | Write a typed location |
equalPointer(left, right) | Compare canonical typed-location identity |
hashPointer(pointer) | Hash canonical typed-location identity |
bindPointer(identity, read, write) | Bind a target/provider storage identity to explicit accessors |
projectPointer(pointer, fromSource, toSource) | Project a typed location through reversible conversions |
projectPointer may preserve an optional pointer. Its conversions are part of
the exact projection contract; targets do not infer them from F and T.
Raw-pointer identity operations
| Export | Meaning |
|---|---|
equalRawPointer(left, right) | Compare raw-pointer identities, including undefined |
hashRawPointer(pointer) | Hash raw-pointer identity, including undefined |
Raw-pointer identity does not authorize dereference or pointer arithmetic. An arbitrary object is not a memory address. Raw addresses require an exact provider contract or a layout-backed typed-location conversion. The shared memory contract describes those operations; each target must prove its storage, layout, lifetime and safety requirements before emitting them.
Layout and raw-memory source contracts
These declarations and their immutable source facts are implemented. C# and
Rust support layout observations, exact address-integer conversions, byte
offsets, raw identity, and keepAlive. Typed-storage conversion with
toRawPointer and reinterpretRawPointer also works for closed scalar layouts.
A checked source fact alone does not prove native storage or lifetime safety.
| Export | Source contract |
|---|---|
memoryLayout<T>(abi, size, alignment, stride, ...fields) | Describe scalar or record storage using a registered ABI token and constant dimensions; not a fixed-array descriptor |
memoryArrayLayout<T, N>(abi, size, alignment, stride, elementLayout: MemoryLayout<T>, length: N): MemoryLayout<FixedArray<T, N>> | Describe a fixed array using one exact child layout and an extent matching the selected type |
memoryField<T, TField>(select, offset, alignment, fieldLayout) | Select a non-optional physical field and its exact child layout without executing the selector |
sizeOf(layout) | Observe the selected byte size |
alignOf(layout) | Observe the selected byte alignment |
strideOf(layout) | Observe the stride between whole values of the selected layout |
fieldOffsetOf(layout, select) | Observe the offset of one exact selected field |
toRawPointer(pointer, layout) | Request the address of the same typed storage, retaining its required owner |
reinterpretRawPointer(raw, layout) | Interpret an address as the canonical Pointer<T>, not NativePointer<T> |
offsetRawPointer(raw, byteOffset, abi) | Offset in bytes using an exact integer domain |
rawPointerToAddressInteger<TAddress>(raw, abi) | Convert to an explicitly selected uint32 or uint64, without retaining ownership |
addressIntegerToRawPointer<TAddress>(address, abi) | Recover an address from the exact unsigned domain, without manufacturing ownership; the type argument may be inferred from the operand |
keepAlive(value) | Require reachability through this call, not pinning |
keepAlive(value) emits global::System.GC.KeepAlive(value) for a C# reference
owner. Rust borrows the value without consuming or cloning it; the owner
retains its native drop scope. Neither operation pins storage, reconstructs an
owner from address bits, or grants an unsafe context.
Every physical field explicitly selects its own layout. For example:
interface Header { count: uint32 }
const word = memoryLayout<uint32>(abi, 4, 4, 4);
const header = memoryLayout<Header>(abi, 8, 4, 8,
memoryField((value: Header) => value.count, 4, 4, word));
The child type must match the selected field type. Parent and child layouts must use the same registered ABI identity and fingerprint. Complete field extents must fit and cannot overlap; padding is allowed. Placement alignment is explicit, including packed placement. Nested layouts retain their selected child descriptors; Tsonic does not search for a layout with a matching name or type. The descriptor dependency graph must be acyclic and remain within the supported 128-level nesting and 131,072-value snapshot limits. These source checks do not by themselves establish a native record representation.
An array descriptor selects one child, even for nested arrays, arrays of records,
or zero elements. Using the same abi and word as above:
const words = memoryArrayLayout<uint32, 2>(abi, 12, 4, 16, word, 2);
Here sizeOf(words) is 12 and strideOf(words) is 16; element spacing remains
strideOf(word), which is 4. The two elements occupy 8 bytes, with explicit
trailing padding. Whole-array dimensions are non-negative safe-integer byte
quantities within the selected address width. For exact count N, occupied
bytes are zero when N is zero, otherwise
(N - 1n) * BigInt(elementLayout.stride) + BigInt(elementLayout.byteSize).
This calculation is exact and must fit the whole-array size. Outer stride must
contain the size and preserve alignment; a nonempty array must preserve its
child’s alignment. A zero-count array still validates the child type, ABI and
acyclic graph. Huge counts with zero-sized, zero-stride children can therefore
describe zero-byte storage without expanding the count into metadata elements.
The length argument must be a proven constant with the same exact value and
number/bigint runtime base as selected N; a type assertion cannot excuse a
different value. memoryLayout<FixedArray<T, N>>(...) is rejected because it
omits the physical child. Neither builder allocates an array or supplies a
native codec. C# and Rust reject raw conversion or physical backing requiring
an inline array, including arrays nested in records; compile-time layout
observations remain separate from that unsupported native representation.
Consumers read TsonicMemoryLayoutFact through @tsonic/source-core/facts:
kind: "value" has fields, whereas kind: "array" has fixedArray,
elementLayoutExpression and elementLayout, with no synthetic fields. The
canonical fixedArray owns the only count (length: bigint), its
lengthRuntimeBase, resolved sourceType and elementSourceType, and optional
authored elementType evidence. Selected memory-type identities preserve
source primitive distinctions and cross-file equivalence; equal checker
carriers alone do not establish that an element is the intended primitive.
Snapshots, finalized observations and backing comparisons follow the one child
edge, not one node per element. The existing graph limits still apply.
For example, given a registered little-endian, 64-bit ABI token exported by
example:abi, both targets preserve this local’s storage:
import { abi } from "example:abi";
import { memoryLayout, addressOf, toRawPointer, reinterpretRawPointer,
storePointer, unsafeContext } from "@tsonic/core/lang.js";
import type { uint32 } from "@tsonic/core/types.js";
const layout = memoryLayout<uint32>(abi, 4, 4, 4);
function write(): uint32 {
unsafeContext();
let value: uint32 = 1;
const raw = toRawPointer(addressOf(value), layout);
const pointer = reinterpretRawPointer(raw, layout);
if (pointer !== undefined) storePointer(pointer, 7);
return value; // 7
}
Analysis gives the demanded local stable native backing before emission. Every
read and write uses that same storage. allocatePointer origins can receive the
same backing. Undemanded locals and logical pointers keep their usual form.
Current physical layouts support signed/unsigned 8-, 16-, 32-, 64- and 128-bit
integers, native-width integers, and 32-/64-bit floats; C# also supports float16.
Scalar size must match its selected representation and have no fields. A
physical record requires a complete value-type contract: a source-defined C#
struct, or a native provider’s explicit complete field identities and value
carrier. Ordinary reference objects cannot be decoded as value records. Record
codecs read and write the selected scalar leaves at their declared offsets;
they do not copy native struct padding or assume the process uses those offsets.
Nested field placement can be packed independently of its standalone layout.
Expanded codecs are limited to 131,072 value occurrences.
Physical operations check process address width, byte order, bounds of retained
allocations, and the selected alignment. These checks do not make an external
address valid: explicit unsafe code must ensure its storage remains initialized,
writable and alive for every resulting alias.
By-value function and method parameters can use the same backing. Returning their address retains the callee’s parameter slot, not the caller’s variable. Pointers stored in closed local arrays and data-property objects also retain their backing through local aliases, binding replacement, and simple element or property assignments. Analysis checks every possible stored pointer; a logical projection cannot acquire native backing by being put in a container. Transporting a pointer is separate from addressing the container’s own storage.
Required mutable data fields of compiler-owned reference objects can also receive native backing. For example:
const cell: { value: uint32 } = { value: 7 };
const alias = cell;
const pointer = addressOf(cell.value);
toRawPointer(pointer, layout);
storePointer(pointer, 9);
alias.value = 11;
Both assignments update the same physical field. Independently taking
addressOf(alias.value) gives the same location. Reassigning cell to a new
object does not retarget the old pointer. The retained location keeps its
storage alive after the original local returns. This storage choice does not
change the field’s public value type or undemanded object shapes.
Dense native arrays have the same retained-location behavior when analysis can close all local aliases and uses:
let values: uint32[] = [7, 8];
const alias = values;
const pointer = addressOf(values[0]);
toRawPointer(pointer, layout);
alias[0] = 11;
values = [99];
loadPointer(pointer); // 11, from the original allocation
The selected element stride determines the distance between adjacent locations. Ordinary element reads and writes use that allocation too. Bounds are checked. The current proof admits initialized block-local bindings, dense non-spread literals, local aliases, standalone binding replacements and element accesses. It rejects collection methods, property operations, resizing, deletion, captures and array escapes through calls, returns or other containers. It does not change undemanded arrays or substitute native arrays for JS-surface collections.
Optional/accessor fields, arbitrary native object fields, escaped pointer
containers and open caller boundaries still lack complete native-backing proofs.
Logical callback projections do not establish physical backing. C# also rejects
passing promoted locals, fields or elements as managed ref/out.
Native providers can return the canonical raw carrier with an explicit storage
lease. C# runtime code calls RawPointer.FromExternal(address, byteLength, owner);
Rust runtime code calls the unsafe RawPointer::from_external(address, byte_length, owner). These are native runtime APIs, not additional TypeScript markers. The
provider’s selected return contract must still identify RawPointer exactly.
The owner must actually keep initialized, writable storage at a stable address until all aliases are released. For example, a C# provider exposing a managed array must retain a real pin, not merely the array. Rust providers must obey native aliasing and access rules. Tsonic does not infer either obligation. Offset aliases and typed views retain the same owner and bounds. Extracting address bits drops that relationship; converting the bits back cannot recover it. Retaining a descriptor handle and decoding a pointer field from physical bytes are separate operations; the latter still requires a complete descriptor and backing contract.
Ordinary pointer-returning functions, methods and callbacks retain their exact pointee representation without an added return annotation. The empty branch returns the target’s undefined representation, not an allocated pointer:
function maybe(flag: boolean) {
if (flag) return allocatePointer<uint32>(1);
}
const present = maybe(true);
const absent = maybe(false); // undefined
Selected generic helpers that return their pointer arguments also preserve
the arguments’ exact pointee evidence. This is not general inference through
arbitrary generic bodies. For example, this return query does not close the
pointee of an operation inside a generic body, such as allocatePointer<T>(value).
An unresolved T is not replaced with a guessed native scalar.
An ABI provider supplies the token declaration and a dataLayouts
contribution containing its exact provider identity, version, fingerprint,
byte order and address width. Source-core validates the registration and owns
the resulting facts. The build machine’s architecture is not an ABI token.
Layout dimensions must be non-negative safe integers; alignment is a positive
power of two. Native field extents and storage compatibility still require
target proof.
Layout builders and their immutable aliases are compile-time metadata, not runtime objects. The targets erase their declarations and replace observations with the selected native-unsigned constants. A descriptor cannot be returned from an ordinary function, put in a runtime container, or passed to an ordinary function. Field selectors are never executed to discover offsets.
Byte offsets accept exact signed and unsigned integer markers, including
bigint-backed widths. Unmarked in-range integer constants are also accepted.
An arbitrary number or bigint variable is not an integer-domain proof.
Optional pointer conversions preserve undefined; address conversion maps
it to zero, and integer zero maps back to undefined.
Address integers use uint32 (number) for a 32-bit ABI and uint64
(bigint) for a 64-bit ABI. They do not use the number-backed nativeUint.
For example, with a registered 64-bit abi token:
const address: uint64 = 9007199254740993n;
const raw = addressIntegerToRawPointer(address, abi);
const exact: uint64 = rawPointerToAddressInteger<uint64>(raw, abi);
This round trip works on both native targets with a registered ABI matching
the executing process. Both operations retain the exact unsigned width and
number/bigint representation.
Raw-to-integer requires the explicit type argument; the destination annotation
does not select it. Integer-to-raw can infer it from an exactly annotated
operand, or accept an explicit argument with an integral constant, such as
addressIntegerToRawPointer<uint64>(9007199254740993n, abi).
Signed types, plain number/bigint variables, mismatched ABI widths and known
out-of-range constants are rejected. Targets must range-check values that
are not proven constant and must never convert 64-bit address bits through
number. An integer contains address bits only: converting it back does not
establish live storage, alignment, pinning or ownership.
Native-pointer operations
| Export | Meaning |
|---|---|
loadNativePointer(pointer) | Dereference a native typed pointer |
storeNativePointer(pointer, value) | Store through a native typed pointer |
offsetNativePointer(pointer, elementOffset) | Offset by pointee elements, not bytes |
Native pointer access requires an explicit safety context when the target language requires one.
Safety builders
| Form | Meaning |
|---|---|
unsafeContext() | Marks the remainder of the containing lexical block as unsafe |
unsafeContext(expression) | Marks exactly one expression as unsafe |
safety<T>().requiresUnsafe() | Declaration-level unsafe-call requirement |
safety<T>().safe() | Declaration-level safe contract |
.method(selector) | Select a method declaration |
.property(selector) | Select a property declaration |
.indexer(selector) | Select an index declaration |
.constructor() | Select a constructor |
.getter() / .setter() | Select one accessor independently |
Example:
safety<NativeBuffer>()
.method((buffer) => buffer.read)
.requiresUnsafe();
export function read(pointer: NativePointer<int32>): int32 {
return unsafeContext(loadNativePointer(pointer));
}
Lexical unsafe context, declaration-level requires-unsafe, and native project permission are independent controls.