Limits
This list shows what version 1 does not support.
- The words
thread_local,friend,mutableandvolatileon a member of a.hotclass. Astaticmethod works (see Static methods). Astaticfield does not. Use a module variable (a variable at file level). - Coroutines other than
asyncmethods. See Async and await. - Generics in script code. You can use C++ templates.
- An exception struct (a struct with the base
ScriptError) as the type of a field, a module variable or an element of a container. Use it only forthrowandcatch. A.cppscript has no exception struct. The only base of an exception struct isScriptError(or another exception struct). See Script exception types. - Reload while a worker thread runs script code, in the default
StopTheWorldmode. The commit waits for the frame boundary. TheEntryCountersmode allows it (see Internals). - A stored
refparameter,reflocal orT*. A borrowed value cannot go to a field. The option--no-strict-borrowedmakes this a warning forT*. A storedrefis always an error. See Borrowing. - The check of a
ref(see “Lifetime of a ref” in Type design) has these limits:- It sees direct mutation only: a call of a method name from the shape list, an assignment,
&c, and a bare name passed to areforoutparameter. - It does not check a callee with 3 or more name parts, such as
p.items.push_back(x). - A
refinto a field of another object with 3 or more name parts, such asref r = a.b.c, uses the rootaonly. - A
varlocal or areflocal of an unknown type counts as a handle.var v = Vec3(); ref r = v.x; v = Vec3();is a false error. - A field of a type that is not a builtin scalar (a struct, an enum, a native type) counts as able to change shape after a
refcall. This can give a false error. - After a call of a
refmethod, a shape change of any reshapeable field ends theref, even a field that the method does not touch. This is a false error. - The message for a replaced owner says “elements of” the owner, also when the owner is a handle or a
T?. - It does not see mutation by another method of the same class,
std.swap(a, b), or a free function that takes the container. - A native
T&getter cannot bind to aref. Copy the value, or use an index. - A
refcall on an object of another class counts as a temporary. ref r = xs[await Idx()]is allowed, because the ref starts after its own statement. Another live ref gives the error about that ref.- The check of
foreach (var x in c)(see Runtime errors) sees only a range that is a local or a field ofthis, such asxsorthis.xs. It compares the name of the root and does no alias analysis. A change through another name, a call of a script method, a free function or a lambda is not checked. A local that hides the field name can give a false error. - A
.cppscript keeps C++ semantics and has noreflifetime rules.
- It sees direct mutation only: a call of a method name from the shape list, an assignment,
- The check of a view (
T[]andstd.string_view, see Views) has these limits:- It tracks a view local with a declared type and a source that is a name or a field of
this. Avarview, a view from a call (var s = v.subspan(1)) and a view of an element are not tracked. - A view local over a field (
std.string_view w = this.label;) is tracked as a view of the field. A literal initializer of the field does not make it safe. - It follows the order of the text. A shape change at the end of a loop body and a use at its start are not seen. A shape change in one branch makes a use in another branch an error, unless the branch ends with
return,break,continueorthrow. - It sees direct mutation only, as the
refcheck does (see above). A change through an alias, another method or a free function is not seen. - A view that a lambda captures, a view parameter of an
asyncmethod, and a view that a native function stores are not checked. The translator cannot tell a stored closure from an immediate call (a sort comparator). The owner can end first, and the view then points to freed memory. Copy the values (std.vector<T>,std.string) in such code. - A native function that takes a
ref, aT&or a view can keep it. The checker cannot see this. Theas_refcheck only tests for null at the call. - A view in a container field of a native type or in a tuple is not checked. A
std.string_viewthat a script call returns from a native getter has the lifetime that the native code gives it.
- It tracks a view local with a declared type and a source that is a name or a field of
- A raw
T*in a local or a parameter keeps its target address after a native free. ACheckedPtrfield is checked, a local or a parameter is not. A nativeT*that the host frees while a script frame holds it is undefined behavior. This includes a frame that anawaitsuspends. A reload does not free a native object. Keep aCheckedPtror a handle, and check it again after a call that can free the object. - A script class, struct or enum with the name of a C++ type from a
usingheader. Rename the script type. - Shared state in an
inlinefunction, aninlinevariable or a template static member of a host header. The script modules build with hidden symbol visibility. Each module gets its own copy of that state, and the host does not share it. Put such state in a.cppfile of the host, or mark it exported. - A namespace block in a
.hotscript. Useusing ns;for a native namespace. - A call of a free function by name from the host through
Loader::thunk. Call a method of a class. - A function as a value, such as
var f = Add;. Call it, or write a lambda. - An
asyncfree function or anasyncstatic method. An async function needs an object. - A collection literal without a declared type:
var x = [1, 2];is an error. - A
.cppscript has no null or bounds checks. The translator keeps a.cppbody as C++ text and does not parse expressions. The checks cover.hotscripts only. front(),back(),top(),pop(),pop_back()andpop_front()on an empty container throwIndexOutOfRange, also withHOT_RUNTIME_CHECKS=0. The check covers a call on a name or a member access. It does not covera?.back(),begin(),end()or*it, nor a receiver of an unknown type (varof an unknown type). The receiver is the first argument of the helper, so it runs before the other arguments.m.at(k)with an integer key and a missing key throwsIndexOutOfRange. A native exception becomes aNativeErroronly inside atryblock that has acatchclause forScriptErrororNativeErrorand no clause for...or a native type. See Runtime errors.- A bounds check covers a C array,
std::array,std::vector,std::deque,std::basic_string(std::string),std::basic_string_view,std::span,hot::spanand a stub with[native, checked_index].std::mapandstd::unordered_mapkeep their ownoperator[], which inserts a key. Useatorfind. Other C++ containers have no bounds check, because the translator does not know their index rules. - A
ScriptErrorin a constructor goes to the code that creates the object. A scriptnewcan catch it. A hostcreatecall gets the C++ exception. The script error handler does not see it, because no object exists yet. - A class with two constructors of the same arity does not coerce
nullfor a[weak]parameter. Give the constructors different arities. The translator cannot choose the constructor from anullargument. - A
noexceptnative virtual that a script overrides and whose result has no default value (a reference, for example). The host must give ahot::noexcept_result<R>specialization for it. Without it an unhandledScriptErrorends the program. - A
noexceptpure virtual native method that no script class implements. The call traps: it reportsabstract method 'C.F' calledand does not end the program. - Script interfaces (see Interfaces) have these limits. An interface cannot inherit from another interface. It has no generics, no fields, no default methods, no
staticmembers and noasyncmethods. A struct cannot implement an interface.[weak]does not work with an interface, andnewof an interface is an error. A.cppscript has no interfaces. The optimized AOT mode does not support them. - Script operators (see Operators) have these limits. There are no
staticoperators, no<=>, no conversion operators and no overloads. An interface and a[native]stub cannot have an operator. A.cppscript has no script operators. An operator on a handle is an error, except==and!=. - A comparison or an operator with a value return cannot write a field of its own type. The checker reports a direct write (
x = 1,x++,this.x = 1, a write to a stored array or container) and a call of a function of the type that writes a field. It does not see a write through a field of a struct type (pos.x = 1), a write to a field of a script parent, a call of a native member function on a field, or a write through a handle. The generated member keepsconst_cast, so such a write is wrong for an object that is trulyconst. - A compound operator (
+=) is not available on a subclass when the binary operator comes from the parent.assign_valuethrows ahot::ScriptErrorwhen the two objects have different classes. - A reload that changes the signature of an interface method on one side only. The translator reports an error and the old generation stays active.
- A native virtual method that a script overrides implements an interface method only when the C++ types are equal.
- Access checks on native members. Only members of known script types are checked, because the translator has no member list for other types. The C++ compiler reports a private native member. The message names the C++ member and points at the script line.
- Two classes whose field initializers build each other with
new. No order of the classes works, sohotcreportsclass '<name>' must come before the class that builds it with 'new'. Use a constructor or a method for one of the two. A field initializer can use a class that comes later in the file when there is no such cycle. - Brace initializers. Write
T(args). ::and->. Write..- A C style cast to a type other than a builtin type or
T*. Usestatic_cast<T>(e). - Member access with
.on an iterator, astd.optionalor astd.unique_ptr. Write(*p).x. - A field of a move-only type, such as
std.unique_ptr<T>. A reload copies each field. Use a handle. - Recursion deeper than 10000 script calls (1000 on Windows). The call throws
ScriptError("stack overflow"), which acatchhandles. The check is always on, also withHOT_RUNTIME_CHECKS=0. Every script member function, static method and free function counts one frame. Change the limit withhot::set_max_call_depth(n)(65519 at most). The macroHOT_MAX_CALL_DEPTHsets the default, but only when you compile the runtime library. The Windows default is lower because the main thread has a 1 MB stack. A script function frame uses about 100 to 400 bytes of stack in a Debug build, and more with many locals. A thread with a small stack (for example 512 KB on macOS) needs a lower limit. - Integer
/,%,/=and%=throwArithmeticErrorfor a zero divisor (see Runtime errors). The check is always on, also withHOT_RUNTIME_CHECKS=0. The translator skips it when the divisor is a nonzero integer literal other than-1, or when an operand has a float type or a script class type.INT_MIN / -1wraps and does not trap. - Integer
<<,>>,<<=and>>=throwArithmeticError("shift count out of range")for a negative count or a count not less than the bit width of the promoted left operand. The check is always on. The translator skips it when the count is a literal in range for a known left type, or when an operand has a float type or a script class type. A left shift wraps. A right shift of a negative value is arithmetic. - Signed overflow wraps because the script compile has
-fwrapv. The flag is in the JIT backend, the Clang files backend and the AOT library (hot_add_scripts). MSVC has no such flag: an MSVC build does not guard signed overflow in scripts. MSVC does not exploit it in practice. - A null handle, a null
T*or an expiredCheckedPtrthat a script passes to a nativeT&orconst T¶meter (the plain formF(w)) throwsNullReference. The check (hot::as_ref) is always on, also withHOT_RUNTIME_CHECKS=0. A null value passed to a nativeT*parameter staysnullptr, because null is a valid pointer argument. The member accessp.xand*pon a nullT*or a null handle keep the check ofHOT_RUNTIME_CHECKS: withHOT_RUNTIME_CHECKS=0a null native pointer from a native call is undefined behavior. ACheckedPtrfield is always checked. The cast(T*)pof a pointer or a handle (hot::nonnull) is always checked. - A cast from a float type to an integer type gives the nearest limit for a value out of range, and
0forNaN(hot::float_to_int). It covers(T)f,T(f)andstatic_cast<T>(f)whenTis a builtin integer type or a C++ integer name. It does not cover: an implicit conversion (int i = f;,i = f;,i += f;, a float argument for an integer parameter, areturnof a float), a cast to an alias of an integer type (using Id = int;), a cast tobool, and a cast that a native header writes. A.cppscript has no shift check and no float cast check, because the translator does not change it. It gets-fwrapv. - The compound helpers
hot::div_assign,hot::mod_assign,hot::shl_assignandhot::shr_assigntake the target by reference. A bit-field target (s.bf <<= 1,s.bf /= n) does not compile. Writes.bf = s.bf << 1. - A user
operator<<oroperator>>must be findable by argument-dependent lookup from the namespacehot, because<<becomeshot::shl. Put the operator in the namespace of its argument type. - The frame count has gaps. A closure body, a coroutine body, and a free function or static method of a
.cppscript do not count their frames, so a recursion only through them can still overflow the stack. A destructor does not count its own frame and cannot throw. While an exception unwinds the stack, a script function that a destructor calls can go 15 frames over the limit. - More than 64 nested statements, 15 nested lambdas or 256 operators in one expression. Each level uses parser stack, so the limits are fixed. The parser reports
maximum nesting depth exceeded. - macOS. It is untested. Windows works with MSVC, and the CI tests it on Windows (see Windows).
- A real Visual Studio or gdb debugger session. It is untested. An lldb session and an lldb-dap session run in the tests on Linux when lldb is installed.
A .cpp script (see C++ scripts) has more limits. It has no properties, no script virtual methods without override, no script-class handles and no new, and no script inheritance.
What can still crash
A HotScript program does not crash the process through an invalid access, with these exceptions. Each item has its details in the list above. The table of the checks that exist is in Checks against crashes.
- A raw
T*in a local or a parameter, after a native free. This includes a frame that anawaitsuspends. - A native function that stores a
ref, aT&or a view. A view that a lambda or anasyncmethod keeps. - A change of a container through an alias, another method or a free function, while a
ref, a view or aforeachuses it. - A
.cppscript. It has no null, bounds, shift, float cast orreflifetime checks. - A null native pointer from a native call, when you build with
HOT_RUNTIME_CHECKS=0. - A container with no bounds check,
a?.back(),begin(),end()and*it. - An implicit float to integer conversion that is out of range.
- Signed overflow in an MSVC build.
- A recursion only through a closure, a coroutine or a free function of a
.cppscript. A thread with a small stack. - A crash inside native code that the script calls.
Limits
These limits come from the type system and the null analysis.
- A member-level
[nonnull]is not available in a stub. A stub has no members. Use[nonnull]on a whole class. - The analyzer does not check an argument to a
[nonnull]parameter. h.reset(rawptr)is always an error. A shared pointer cannot come from a raw pointer.$NativeBase b = this;is an error, unlessenable_shared_from_thisis known for the native base.T[]ishot::span<T>and works with C++17. The optionHOT_VIEW_TYPE=stdselectsstd::span<T>, which needs C++20. The choice is one constant for the whole build. See The view type.