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.
Operations, builder methods and authored descriptor keys use lowercase without
underscores; operation names shorten pointer to ptr. Type names and authored
bindings keep their exact spelling. Only the names documented here are exported;
there are no former-name aliases or positional layout-construction overloads.
@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 |
MemoryFieldBinding<T> | An explicit binding of one physical field to an existing typed location |
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. The native target retains its
array length carrier independently of the metadata literal’s spelling; the
source type does not require a bigint allocation or a floating conversion.
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>() | 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(() => new InAttribute());
The selected provider owns the target attribute identity and legal values.
The argument is an inline, synchronous, zero-parameter arrow containing one
checked call or construction. Constructor arguments use ordinary TypeScript
type checking. A stored callback, block body or bare attribute type is not an
attribute application. The lambda is annotation syntax; no callback runs or
allocates at runtime. Native attribute constant and placement rules still apply.
This example uses C#’s InAttribute on a parameter. Import it from
@tsonic/dotnet/System/Runtime/InteropServices.js.
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 |
allocateptr(initial) | Allocate independent typed storage |
loadptr(pointer) | Read a typed location |
storeptr(pointer, value) | Write a typed location |
equalptr(left, right) | Compare canonical typed-location identity |
hashptr(pointer) | Hash canonical typed-location identity |
bindptr(identity, read, write) | Bind a target/provider storage identity to explicit accessors |
projectptr(pointer, fromSource, toSource) | Project a typed location through reversible conversions |
viewptr(pointer, read, write) | Bind new accessors to the same referent without implicitly reading or writing the original pointee |
projectptr may preserve an optional pointer. Its conversions are part of
the exact projection contract; targets do not infer them from F and T.
viewptr<F, T> returns Pointer<T> for a non-optional Pointer<F>, or
Pointer<T> | undefined for an optional base. It preserves nil, location
identity, position, owner and any established raw-address provenance. It does
not allocate replacement storage, reinterpret bytes or prove a native backing.
The arguments evaluate once in source order. Construction invokes neither
callback. Loading the resulting pointer calls only read(): T; storing calls
only write(value: T): void. Callback exceptions propagate. Nil still evaluates
the argument expressions, but invokes no callbacks. In contrast,
projectptr loads F before fromSource(F) and writes the result of
toSource(T) back to F.
For example, an empty view can retain an end position without reading an element there:
import type { Pointer, FixedArray } from "@tsonic/core/types.js";
import { viewptr } from "@tsonic/core/lang.js";
function emptyView<T>(
end: Pointer<T> | undefined,
empty: FixedArray<T, 0>,
): Pointer<FixedArray<T, 0>> | undefined {
return viewptr(end, () => empty, replacement => { empty = replacement; });
}
The region owner supplies end; a valid position does not grant permission to
dereference a nonexistent element. The zero-length callbacks need no element
load or byte codec. Raw address observation still needs an explicit layout and
the existing backing/lifetime proof. These are shared source contracts. Native
C#/Rust lowering of callback-backed views is not implemented; a consumer must
implement their exact semantics or reject them, never substitute
projectptr or a new allocation.
Raw-pointer identity operations
| Export | Meaning |
|---|---|
equalrawptr(left, right) | Compare raw-pointer identities, including undefined |
hashrawptr(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
torawptr and reinterpretrawptr also works for closed scalar layouts.
A checked source fact alone does not prove native storage or lifetime safety.
| Export | Source contract |
|---|---|
memorylayout<T>(descriptor) | Describe scalar or record storage using datalayout, bytesize, bytealignment, stride, and fields; not a fixed-array descriptor |
memoryarraylayout<T, N>(descriptor) | Describe FixedArray<T, N> using datalayout, bytesize, bytealignment, stride, elementlayout: MemoryLayout<T>, and length: N |
memoryfield<T, TField>(descriptor) | Select a non-optional physical field using select, byteoffset, bytealignment, and fieldlayout: MemoryLayout<TField>, without executing the selector |
bindmemoryfield<T, TField>(field, pointer) | Bind that exact field layout to a non-nil Pointer<TField> |
bindmemoryrecord<T>(layout, ...bindings) | Construct a typed field view with exactly one explicit location binding per physical field |
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 |
torawptr(pointer, layout) | Request the address of the same typed storage, retaining its required owner |
reinterpretrawptr(raw, layout) | Interpret an address as the canonical Pointer<T>, not NativePointer<T> |
offsetrawptr(raw, byteOffset, abi) | Offset in bytes using an exact integer domain |
rawptrtoaddressinteger<TAddress>(raw, abi) | Convert to an explicitly selected uint32 or uint64, without retaining ownership |
addressintegertorawptr<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 |
Each construction takes exactly one direct inline object with all and only its
listed keys, in any order. fields is an inline array, including [] for a
scalar or empty record. Descriptor objects and arrays are erased checked metadata,
not runtime allocations. Stored descriptor objects/field arrays, spreads, holes,
computed keys, methods, getters, setters and outer quotation callbacks are not
accepted. Identifier or quoted-string keys and ordinary property shorthand use
the same exact schema; each value must independently satisfy its existing proof
rules. In particular, select remains an inline typed member-selection lambda.
Immutable aliases of already-proven ABI, child-layout and field metadata values
remain valid; mutable aliases and asserted replacement identities do not.
Queries and mixed pointer/layout operations retain their direct operands.
Explicit field locations
An ordinary getter forwarding to a pointer does not make the getter’s property
location equal to that pointer. bindmemoryrecord makes this relationship
explicit, using existing memoryfield identities rather than property names:
interface Header { count: uint32; tag: uint32 }
const countField = memoryfield({ select: (value: Header) => value.count, byteoffset: 0, bytealignment: 4, fieldlayout: word });
const tagField = memoryfield({ select: (value: Header) => value.tag, byteoffset: 4, bytealignment: 4, fieldlayout: word });
const layout = memorylayout<Header>({ datalayout: abi, bytesize: 8, bytealignment: 4, stride: 8, fields: [countField, tagField] });
const logical: Header = { count: 3, tag: 5 };
const before = addressof(logical.count);
const count = bindmemoryfield(countField, before);
const tag = bindmemoryfield(tagField, addressof(logical.tag));
const physical = bindmemoryrecord(layout, tag, count);
Here abi is the registered ABI token and word its uint32 layout.
physical.count reads and writes through before; its field address denotes
that same location. Writing it does not touch logical.tag. Bindings may be
passed in a different order from the layout, but every field must appear exactly
once. Missing, duplicate and unrelated fields are rejected. Record layouts must
cover all required data members, not methods, accessors or open indexers.
Scalar and fixed-array layouts cannot be passed as record layouts.
Each binding captures its pointer once; construction does not load the field.
Reassigning a variable that supplied the pointer does not retarget the binding.
The field pointer’s exact memory type must match its child layout, even when
TypeScript represents both int32 and uint32 as number.
A field can point to another bound record or a fixed array. Its existing child
layout supplies the nested fields, element layout, exact count and stride. A
region can form each physical element with viewptr(element, read, write)
and construct its record with bindmemoryrecord inside read. No relation is
inferred from an arbitrary array getter or a record copy.
For logical-to-physical conversions, compose field bindings with
projectptr or viewptr at the root. The explicit converters still own
aggregate copying and descriptor replacement. Binding does not merge separate
allocations or retarget previously copied descriptors. When a physical root is
exposed as raw memory, its field offsets must agree with these retained logical
locations. A consumer must reject contradictory owners/positions rather than
overwrite provenance. This does not prove contiguous storage for independently
allocated native fields.
Field/layout operands remain compile-time metadata. Field bindings contain
runtime pointers and must not be erased. Metadata and binding arguments must
resolve to their exact construction or immutable aliases; casting an arbitrary
object to MemoryFieldBinding<T> supplies no relation.
Targets consume selectTsonicPointerView, selectTsonicMemoryFieldBinding and
selectTsonicMemoryRecordBinding from @tsonic/source-core/facts. Each takes the
public AST reader, finalized source-fact resolver and exact call node. A
resolved result includes selected operands and types; record results include
the exact field-to-pointer join and recursively finalized layouts. Incomplete
or contradictory retained evidence is rejected. These readers do not call the
checker or inspect callee spelling. The corresponding keys are
tsonicPointerViewFactKey, tsonicMemoryFieldBindingFactKey and
tsonicMemoryRecordBindingFactKey, all owned by source-core.
The shared declarations and facts do not implement a target runtime. C#/Rust do not yet lower explicit bound records. Native consumers must independently prove backing and lifetime safety or reject the request.
Layout observations and storage
Layout queries return nativeUint byte quantities and can be used directly as
raw byte offsets. With a registered abi and a RawPointer | undefined value
named raw:
const word = memorylayout<uint32>({ datalayout: abi, bytesize: 4, bytealignment: 4, stride: 4, fields: [] });
const next = offsetrawptr(raw, sizeof(word), abi);
The same applies to alignof, strideof and fieldoffsetof, including immutable
aliases of their results. The query must resolve to an exact layout. Integer
width checks still apply: asserting a byte count of 256 as uint8 does not make
it fit, and an authored plain number does not supply an integer domain.
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>({ datalayout: abi, bytesize: 4, bytealignment: 4, stride: 4, fields: [] });
const header = memorylayout<Header>({
datalayout: abi,
bytesize: 8,
bytealignment: 4,
stride: 8,
fields: [memoryfield({ select: (value: Header) => value.count, byteoffset: 4, bytealignment: 4, fieldlayout: 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>({ datalayout: abi, bytesize: 12, bytealignment: 4, stride: 16, elementlayout: word, length: 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, torawptr, reinterpretrawptr,
storeptr, unsafecontext } from "@tsonic/core/lang.js";
import type { uint32 } from "@tsonic/core/types.js";
const layout = memorylayout<uint32>({ datalayout: abi, bytesize: 4, bytealignment: 4, stride: 4, fields: [] });
function write(): uint32 {
unsafecontext();
let value: uint32 = 1;
const raw = torawptr(addressof(value), layout);
const pointer = reinterpretrawptr(raw, layout);
if (pointer !== undefined) storeptr(pointer, 7);
return value; // 7
}
Analysis gives the demanded local stable native backing before emission. Every
read and write uses that same storage. allocateptr 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);
torawptr(pointer, layout);
storeptr(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]);
torawptr(pointer, layout);
alias[0] = 11;
values = [99];
loadptr(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 allocateptr<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 allocateptr<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 = addressintegertorawptr(address, abi);
const exact: uint64 = rawptrtoaddressinteger<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
addressintegertorawptr<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 |
|---|---|
loadnativeptr(pointer) | Dereference a native typed pointer |
storenativeptr(pointer, value) | Store through a native typed pointer |
offsetnativeptr(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(loadnativeptr(pointer));
}
Lexical unsafe context, declaration-level requires-unsafe, and native project permission are independent controls.