Skip to Content

Type Conversions

MushScript offers several ways to convert between types.

T(v): numeric conversion and parsing

Conversions are written as calls with the type name in function position; the semantics dispatch automatically on source and target type. Numeric conversions never fail, and float to integer truncates toward zero (no rounding):

val trunc = int64(3.9); print(trunc);
3
  • Integer types do not take decimal literals, not even 3.0 (val w: int = 3.0;).
  • When a float constant converts to a narrower integer, the range is checked at compile time: int16(-40000.0f64) reports MS4211.
  • char conversions go through the code point: int('A') takes the code point, char(66) builds the character for it.

String sources enter the parsing channel: int("42") gives 42, and bool("True") / decimal64("1.5") work the same way. A parse failure throws System.ConversionError; to dodge it use try? (failure gives null):

print(try? int("42")); print(try? int("x")); print(try? bool("True"));
42 null True
Note

Only string sources are accepted for bool / string targets.

User conversions: top-level conversion

User conversions are declared at module top level; conversion declarations inside type bodies and extensions are no longer allowed (writing one there produces a targeted error pointing you to move it to the top level).

A conversion always takes exactly one parameter and must declare its return type. conversion is the explicit form, consumed only through T(v). An implicit conversion takes effect automatically in implicit positions (assignment, argument passing, returns, operand position), can also be called explicitly with T(v), and once declared implicit can no longer be combined with fail.

class Meters { public var v: int = 0; } public conversion(v: int) -> Meters { return new Meters() { v: v }; } public implicit conversion(m: Meters) -> int { return m.v; } func main() -> int { val m = Meters(42); val n: int = m; print(m.v, n, int(m), m + 1); return 0; }
42 42 42 43

Cross-module activation is gated by import: import moduleA; activates the conversions it declares; without the import they are invisible. When two imported modules declare the same pair of conversions, the use site reports an ambiguity error.

The standard way to parse a user type from a string is a fail conversion whose source is string; failure travels the fail channel, and the T(v) call site must consume it with try/capture:

struct Celsius { public val C: float; public init(c: float) { C = c; } } public conversion(s: string) fail System.ConversionError -> Celsius { return new Celsius(try float(s)); } func main() -> int { val c = try? Celsius("36.5"); print(c != null); if c != null { print(c.C); } return 0; }
True 36.5

is, as, and the checked conversion T(v)

Use this table to pick the right conversion form:

FormReturnsFailure behavior
is TboolFalse
as TT?null
T(v) checkeda bare Tfails with System.ConversionError, must be consumed

Between numeric scalars, as returns a bare T:

val viaAs = 3.9 as int64; print(viaAs);
3

At compile time the two sides’ relationship is graded: derived to base, implementer to protocol position, same type — always succeeds at zero cost; downcasts and any sources are left to runtime checks.

When two types are connected by neither a base-class chain nor a protocol implementation, T(v) is rejected at compile time. as does not refuse unrelated reference types and gives null at runtime; for cross-domain targets (say, as-ing a custom type to a number) it reports MS3241 at compile time. A checked conversion’s result is guaranteed to be a valid instance and takes no part in null checks.

class Animal { public func Name() -> string { return "animal"; } } class Dog : Animal { public func Bark() { print("woof"); } } func Describe(a: Animal) -> int { val soft = a as Dog; print(soft == null); val hard = try? Dog(a); print(hard != null); return 0; } func main() -> int { Describe(new Dog()); Describe(new Animal()); return 0; }
False True True False

Casts from any sources go through runtime checks, and protocol targets are likewise judged by the implementation chain:

val o: any = new Dog(); print(o is Dog); val d = o as Dog; print(d == null);
True False

When a (source -> target) conversion method is declared, T(v) goes through the conversion channel and does no reference check.

Notes

  • Conversions between integers of the same family use the call form too (as in int8(x)).
  • An explicit conversion checks at compile time that the target type exists; a misspelled type name errors on the spot.
Last updated on October 11, 2026