-
Notifications
You must be signed in to change notification settings - Fork 0
Types and Nullability
Andrew Lind edited this page Sep 22, 2026
·
8 revisions
-
Floatlowers to C++double(64-bit, matching Haxe'sFloaton every official target — neverfloat, which would silently halve the precision); genuine single-precision is available asSingleor hxcpp'scpp.Float32→ C++float(andcpp.Float64→double). -
A floating literal takes the
fsuffix in acpp.Float32context — an argument to acpp.Float32parameter, a local/field initialiser or assignment, areturn, anArray<cpp.Float32>element, or a struct-literal field:SetPerspective(70.0f, 0.1f, 100.0f),float f = 0.1f;. A bare C++ floating literal is adouble, so without the suffix every such line narrows at the conversion and MSVC reports C4305 — the value is identical either way, but a VC6 build is expected to compile clean. A genuineFloat/doublecontext is left alone, and so is arithmetic:Floatarithmetic is double arithmetic whatever it is assigned to, soreturn a * 0.5;keeps itsdoubleoperand and narrows once at the end (see the next bullet). -
A narrowing conversion is an explicit cast. A value stored into a smaller scalar than it has —
(uint16_t)(base + 1)for anIntexpression pushed into anArray<cpp.UInt16>,(float)(a * 0.5)for aFloatreturned ascpp.Float32— is cast rather than left to the implicit conversion, wherever the target type is known (a call argument, an initialiser or assignment, areturn, a container element, a struct-literal field). The conversion happens either way, so nothing changes at runtime: the cast states the intent, keeps MSVC quiet (C4244), and avoids handing VC6 an implicit narrowing of a loop-derived value, which it has been seen miscompiling under/O2. Only genuine narrowings qualify — a smaller destination of the same kind, or a floating value into an integer; a literal is written in the target type already and is not cast. C++ integral promotion counts:n + 1wherenis acpp.UInt16is anintexpression, so storing it back into auint16_tis a narrowing. -
Division semantics are preserved:
/always yieldsFloat, so two statically-known-integer operands divide asdouble(a / b→((double)(a) / b);Std.int(a / b)truncates back, as in Haxe);%with a float operand lowers tofmod(C89<math.h>, portable to VC6 — C++%is integer-only). More broadly, arithmetic with aFloatoperand yieldsFloat(+,-,*,/,%), so avarinferred fromintField / floatFieldis adouble, not a truncatedint. - The full shift set including the unsigned
>>>/>>>=(no C++ spelling — lowered through anunsigned intcast,(int)((unsigned int)a >> b)). -
Null<T>and optional value-structs lower uniformly toT*(with matching heap-allocation at call sites);Map.get(k)lowers to an iterator with an existence check. ANull<T>over a valueT(e.g.Null<String>) is an owned heap pointer:nullisNULL, assigning a value heap-wraps (new T(v), freeing any prior value), a value-position read dereferences (NULL→ a defaultT),!= nullis a real pointer check, and the destructor frees it. -
A plain
Stringis non-nullable — it lowers to a valuestd::string, which has no null. Sos == null/s != nullon a valueStringis a hard error (a value string is never null): test emptiness withs != "", or useNull<String>for a genuinely nullable string. The one exception is an optional?s:Stringparameter, which defaults to"", so a "was it passed?" check legitimately reads ass.empty(). -
finalconstants lower to namespace-scopedstatic const(no#define), namespace-qualified across boundaries. -
A
typedefalias is transparent — it takes on the shape of whatever it names, resolved through before every pointer/reference/container/dispatch decision, while the alias name is kept in the emitted spelling.typedef Color = cpp.UInt32is the primitiveuint32_t(by value, an optional?c:Colorgets a default);typedef Panel = Widgetis the reference class (Panel*,->dispatch, methods/fields resolve through it);typedef Ints = Array<Int>is that container (const Ints¶m — see Statements & Expressions);typedef Vertex = Ptis the value struct (const Vertex&, field access works);typedef Name = Stringkeeps theconst Name&optimization; andNull<Ptr>over a pointer alias stays a single pointer. -
A member's type resolves where it is declared, not where it is used. The field types of a
typedefstruct or class — and a method's return type — are looked up in the scope of the module that declares them, so an unrelated same-named type the using module imports can never hijack them. Withgfx.Mesh = { vertices:Array<Vertex> }meaninggfx::Vertex, a module that also imports aui.Vertexproxy still indexesmesh.vertices[i].xas a value struct (., not->), and a nested literal (line = { a: { x: 1.0, y: 2.0 }, … }) or array-of-struct literal ({ vertices: [ {…} ] }) is built asgfx::Vertex/std::vector<gfx::Vertex>— no explicitly typed local is needed. See Interop via@proxyfor the common way this collision arises. -
Pointer interop types —
cpp.RawPointer<T>/cpp.Star<T>→T*,cpp.ConstStar<T>→const T*, andcpp.Void→void(socpp.RawPointer<cpp.Void>isvoid*).Dynamic/Anyin an emitted position also erase tovoid*. These and the.rawidioms have their own page — see Raw-Pointer Interop.
Hatchet is licensed under the MIT License — see LICENSE. (c) 2026 Andrew Grant Lind
Getting Started
Language Support
- Declarations
- Value Types & Abstracts
- Members & Access
- Statements & Expressions
- Types & Nullability
- Conditional Compilation
- Memory Ownership
Semantics & Interop
Internals