Sage
Tusk Language
Welcome to Sage
Volume (20%) Hide Volume
Topics
Extensions to Delphi

While Tusk offers a high degree of compatibility with Delphi, it also includes a number of extensions and enhancements. This article discusses the most important of these, summarizing them in boxes like this…

Tusk Feature

Tusk is highly compatible with Delphi, but offers a number of optional enhancements.


DateTime Conversions

Delphi is too forgiving when it comes to converting between TDateTime and numeric types. For example, Delphi allows this…

var i: Integer := 1; var j: Integer := 3; var d: TDateTime := i / j;

Tusk is more strict.

Tusk Feature

In Tusk, an explicit cast is required to convert TDateTime to a floating-point type, and for converting an integer or floating-point type to TDateTime.


Specifically, Tusk requires an explicit cast for…

  • DoubleTDateTime
  • SingleTDateTime
  • CurrencyTDateTime
  • any integer type → TDateTime

Converting Dynamic Arrays to Variant

At compile time, Delphi allows converting any dynamic array to Variant. However, at runtime, it requires the array's element type to be compatible with OLE Automation, ruling out types like enums, Boolean, Char, and AnsiChar. For example…

var v: Variant; // These all work fine... v := TArray<Integer>.Create(); v := TArray<WordBool>.Create(); v := TArray<Double>.Create(); // These are all runtime errors in Delphi, // but parse-time errors in Tusk... v := TArray<Boolean>.Create(); v := TArray<Char>.Create(); v := TArray<TYesNo>.Create();

In Tusk, these errors are reported earlier.

Tusk Feature

In Tusk, if a dynamic array doesn't cast to Variant, it is a parse-time error, not a runtime error.


Improved is and as Operators

In Delphi, the is operator works for class types, and the as operator works for class and interface types. Tusk supports these operators for all data types.

For example, consider this Delphi code…

var ls := NewStringList; var v: Variant := ls; if TType.VarSupports(v, IStringList) then Writeln('v is a string list');

In Tusk, the above can be simplified to…

var ls := NewStringList; var v: Variant := ls; if v is IStringList then Writeln('v is a string list');

Improved Operator Clarity

Tusk matches Delphi's operator precedence, which is important for maintaining compatibility between the two languages. However, Tusk requires parentheses in some situations where Delphi does not. For example, in Delphi, the following is valid…

Flag := a < b and c > d;

Delphi interprets the above as…

Flag := (a < (b and c)) > d;

This is almost certainly not what the programmer intended, and is a constant source of trouble in Delphi. In Tusk, the original (without parentheses) is not legal. Instead, Tusk requires you to clarify, using one of these…

Flag := (a < b) and (c > d); Flag := (a < (b and c)) > d; Flag := a < ((b and c) > d);

The first option is the most plausible, but all three are legal.

Tusk does not allow any of the following…

// All illegal in Tusk Flag := a < b and c > d; Flag := a < b and (c > d); Flag := (a < b) and c > d; Flag := a < (b and c) > d; Flag := (a < b and c) > d; Flag := a < (b and c > d);

The following table summarizes the restrictions in Tusk…

Operators Forbid Unparenthesized Operands Notes
=, <>, <, <=, >, >= =, <>, <, <=, >, >=,
and, or, xor, is, in, not in
and, or, xor =, <>, <, <=, >, >=
is =, <>, <, <=, >, >=,
and, or, xor, is, in, not in
applies to right
operand only

Tusk Feature

Tusk requires more parentheses than Delphi, in certain situations, to work around some confusing aspects of Delphi's operator precedence.


One-Line Routines

Tusk offers slightly more concise notation for defining short functions and procedures…

// Traditional approach function Sum(x, y: Integer): Integer; begin Result := x + y; end; // More concise alternative function Sum(x, y: Integer): Integer = x + y; // Traditional approach procedure Display(a, b: Integer); begin Writeln(a, ' + ', b, ' = ', Sum(a, b)); end; // More concise alternative procedure Display(a, b: Integer) = Writeln(a, ' + ', b, ' = ', Sum(a, b));

For a one-line procedure, what follows the = sign is a single statement.

For a one-line function, what follows the = sign is a single expression.

Note

The Result variable is not defined in one-line functions.


Tusk Feature

In Tusk, a function that does nothing other than returning an expression can be written in a more concise "one-liner" notation. This also applies to single-statement procedures.


Inferred Return Type

Like Delphi, Tusk offers type inference when declaring variables…

var a := 123; // implicitly of type Integer var s := 'hello'; // implicitly of type string

Tusk extends this feature to the return type of a function (when using the one-line approach described above)…

// Implicitly returns Integer function Sum(x, y: Integer) = x + y;

Note

This feature is not compatible with recursive functions.

Specifically, in a one-liner function with an inferred return type, it is not legal to reference the function by name.


So, instead of this…

function f(x: Integer) = if x=0 then 0 else x+f(x-1);

you'll need to do this…

function f(x: Integer): Integer = if x=0 then 0 else x+f(x-1);

Tusk Feature

In Tusk, the return type of a (non-recursive) one-liner function can be inferred.


Trailing Commas

Like many languages in the C family, Tusk permits trailing commas and semi-colons…
  • When declaring function/procedure parameters;
  • When specifying arguments to a function/procedure call;
  • When specifying expressions in a case statement;
  • When building a list literal.
  • When building an IPropBag literal.

For example…

function f( x: Integer = 0; y: Integer = 1; z: Integer = 2; ): Double; begin Writeln( 'X is ', x, '; ', 'Y is ', y, '; ', 'Z is ', z, '; ', ); ProcessList([x, y, z,]); ProcessObject({ i=5, j=6, jk7, }); end;

Tusk Feature

In Tusk, trailing commas and semi-colons are allowed.


Topmost Identifiers

Tusk offers a way to access the topmost definition of an identifier: the top operator (unary /). For example…

const s = 'hello'; procedure Work; var a: Integer := 123; s: string := 'goodbye'; begin Writeln(a); // prints "123" Writeln(s); // prints "goodbye" Writeln(/s); // prints "hello" end; Work;

As the comments in the above example indicate, the top operator denotes the topmost (i.e., outermost) definition of an identifier. Note that the topmost is not necessarily a global, as in this example…

const s = 'hello'; procedure Work; var a: Integer := 123; s: string := 'goodbye'; procedure Helper; var a: Double := 4.5; begin Writeln(a); // prints "4.5" Writeln(/a); // prints "123" end; begin Helper; end; Work;

Above, the top operator finds the topmost definition of a, which is the one local to the Work routine.

Hiding Identifiers

In Delphi code, we sometimes attempt to hide an existing identifier (typically a routine). For example, DSGenUtil contains the following…

type TDSGenUtil_Illegal = class private type TType = record end; end; function Supports( const Value: TDSGenUtil_Illegal.TType): Boolean; deprecated 'Use DSSupports or TType methods';

Above, the Supports routine (defined in SysUtils) is effectively hidden: it cannot be called without a unit override (as in SysUtils.Supports(...)). The routine in DSGenUtil also cannot be called, because it takes an argument that the caller cannot supply (it is a type that's only accessible from within DSGenUtil).

Tusk offers a simpler way to hide an identifier for the remainder of the current scope – the hide directive: {$Hide <identifier>}. For example…

Writeln(Sqrt(100)); // prints "10" procedure Demo; begin Writeln(Sqrt(400)); // prints "20" {$Hide Sqrt} Writeln(Sqrt(64)); // illegal Writeln(/Sqrt(64)); // also illegal end; Writeln(Sqrt(16)); // prints "4"

Above, the identifier Sqrt is hidden half-way through the Demo routine, making it inaccessible (even with the top operator).

Here is another example…

const Prompt = 'hello'; procedure Main; begin {$Hide Prompt} procedure Helper; const Prompt = 'goodbye'; begin Writeln(Prompt); // prints "goodbye" Writeln(/Prompt); // prints "goodbye" end; Helper; end; Main;

Above, in the Helper routine, both Prompt and /Prompt refer to the local identifier – not the global – because Main hides the global. In other words, {$Hide ...} creates a new "ceiling" for the top operator.

The fromuse Statement

As a scripting language, Tusk simplifies Delphi's unit concept.

Tusk offers a dedicated notation to simplify scripts that need to reference several identifiers that aren't in the global namespace. For example, instead of this…

function WrapTask(const v1, v2: MyUnit.TType1): MyUnit.TType2; begin var Temp: MyUnit.TType3; Temp := MyUnit.Compute(v1, v2); Result := MyUnit.Resolve(Temp); end;

The above script can be simplified as follows…

from MyUnit use TType1, TType2, TType3, Compute, Resolve; function WrapTask(const v1, v2: TType1): TType2; begin var Temp: TType3; Temp := Compute(v1, v2); Result := Resolve(Temp); end;

The fromuse statement brings one or more global symbols into scope for the remainder of the current block. The above example is equivalent to this…

type TType1 = MyUnit.TType1; TType2 = MyUnit.TType2; TType3 = MyUnit.TType3; const Compute = MyUnit.Compute; Resolve = MyUnit.Resolve; function WrapTask(const v1, v2: TType1): TType2; begin var Temp: TType3; Temp := Compute(v1, v2); Result := Resolve(Temp); end;

Thus, the fromuse statement merely provides a more concise syntax for making several type and/or const declarations.

Local Variables are Initialized

In Delphi, local variables (of unmanaged type) are not initialized (they have "garbage" initial values). Delphi does warn about this, but only for some types (it does not warn for records or static arrays, for example). Even for supported data types, the warnings are not reliable: take the address of a variable, or pass it by reference, and the warning vanishes.

Tusk eliminates this complexity with a simple policy:

Tusk Feature

In Tusk, all variables (global and local) are zero-initialized.


Optional Declarations

Tusk offers a way to declare a type, constant, or variable – but only if the identifier is not already defined. This is especially handy when using include files.

For example, imagine an include file containing…

// In Helper.tusk... var ReadOnly := True;

Then, the main file looks like this…

// In Main.tusk... {$Include Helper} var ReadOnly := False;

Above, the definition of ReadOnly replaces the one from the include file. Assuming the goal is for the main file to default ReadOnly to False, but allow the include file to set it to True, then we need an optional declaration in the main file…

// In Main.tusk... {$Include Helper} var? ReadOnly := False;

Note the question mark above. It means that the var declaration is optional: if ReadOnly is already defined (whether True or False), then this declaration is a do-nothing statement. On the other hand, if ReadOnly is not already defined, then this acts as a normal var declaration.

Similarly, Tusk offers optional type and constant declarations, using the type? and const? keywords.

If a const or var is already defined, it must be the same type (though the values may differ).

Mixing var, const, and type is not allowed.

Tusk Feature

Tusk supports optional declarations, which are especially handy in conjunction with include files.


Initializers in Local var Blocks

Tusk supports initializing variables in local var blocks (those that appears in a routine, before the begin / end block).

For example, the following is valid in Tusk, but not Delphi…

procedure Work; var a: Integer := 123; // fine in Tusk but not Delphi b := 234; // fine in Tusk but not Delphi begin var c := 'hello'; // fine in both Tusk and Delphi end;

Above, the declarations of a and b cannot specify an initial value in Delphi mode, requiring this instead…

procedure Work; var a, b: Integer; begin a := 123; b := 234; var c := 'hello'; end;

Inline type Declarations

Tusk allows inline type declarations, along with const and var

procedure Work; begin type Number = Integer; var f: Number; end;

In Delphi, you would need to move the type declaration outside of the begin / end block…

procedure Work; type Number = Integer; begin var f: Number; end;

Decimal Literals

Tusk offers a syntax for Decimal literals, using the m suffix…

var Price := 12.34m;

TDateTime Literals

Tusk offers a syntax for Decimal literals, using the #(...) notation, borrowed from VDB and MiniCalc

var Expiration := #(1/1/2000 4:15pm);

Object Literals

Tusk supports IPropBag literals defined using curly braces, similar to MiniCalc or JSON…

var Bag := { x=1, y=2 };

For more details, see here.

String Literals

Tusk supports traditional Delphi strings, as well as the newer ''' ... ''' multi-line string literals.

Tusk does offer string literals using double-quotes, but these differ from MiniCalc – they are compatible with C, C++, C#, Java, JavaScript, and JSON.

Tusk Feature

Tusk supports string literals using double-quotes, which are compatible with JavaScript and similar languages.

Because Tusk supports IPropBag literals using curly braces, Tusk is a superset of JSON (every valid JSON file is also valid Tusk, with equivalent meaning).


Of course, Tusk also supports traditional Delphi-like string literals, using single quotes and pound signs.

Exponentiation Operator

Tusk offers the ** operator for exponentiation…

Writeln(2 ** 3); // prints 8

Tusk does not use ^ for exponentiation, as it conflicts with the pointer dereference notation.

The rules for type analysis are as follows…

  1. When either operand is Any or NoCall, the result will be Any;

  2. When either operand is Variant, the result will be Variant;

  3. When both operands are built-in numeric types (Integer, UInt64, Double, etc), the result will be Double;

  4. If either operand is a custom numeric record (Decimal, Rational, BigInt, etc), the other operand must be a record or a built-in numeric type.

    1. If both operands are the same type, the result will be that type as well;

    2. If only one operand is a custom numeric record, the other operand is implicitly converted to that type, which is also the result type (if the conversion is not allowed, the expression is invalid);

    3. If the right operand can be implicitly converted to the left, the result type is the type of the left operand;

    4. If the left operand can be implicitly converted to the right, the result type is the type of the right operand;

    5. Otherwise, the expression is invalid.

Examples…
  • 2.0 ** 2 yields Double;

  • 2m ** 2 yields Decimal;

  • 2m ** Rational(2) yields Decimal
    (Decimal and Rational each convert to the other, so the tie goes to the base, not the exponent);

  • Rational(2) ** 2m yields Rational
    (Decimal and Rational each convert to the other, so the tie goes to the base, not the exponent);

  • BigInt(2) ** BigCardinal(2) yields BigInt
    (BigCardinal casts to BigInt, but not vice-versa);

  • BigCardinal(2) ** BigInt(2) yields BigInt
    (BigCardinal casts to BigInt, but not vice-versa);

  • Rational(2) ** BigRational(2) is a parse error
    (neither Rational nor BigRational convert to the other).

Tusk Feature

In Tusk, the ** operator denotes exponentiation.


First-Class Types

In Tusk, the IType data type represents a type, somewhat like Delphi's PTypeInfo. In Tusk, however, IType is more flexible and powerful. For example…

type INbrList = IList<Integer>; const Nbr = ValueType(INbrList); // Nbr is Integer var z: Nbr := 223; // z is an Integer

Tusk Feature

In Tusk, types may be manipulated as first-class values, using the IType interface.


Indexed forin Loops

Tusk supports Delphi's for loops, including a new feature for the forin loop. Instead of this…

var i := 0; for var v in List do begin Writeln('List[', i, '] = ', v); Inc(i); end;

You can do this…

for var v at var i in List do Writeln('List[', i, '] = ', v);

The index variable, which is introduced with the at keyword, is zero for the first iteration, one for the next, and so on. This variable can be local to the loop, as shown above, or can be an existing variable…

var i: Integer; for var v at i in List do Writeln('List[', i, '] = ', v);

When the variable is local to the loop, you can specify the data type, but it defaults to NativeInt

for var v at var i: Int64 in List do Writeln('List[', i, '] = ', v);

Tusk Feature

In Tusk, the forin loop can include a loop index variable, introduced with the at keyword.


Strict case Statements

Tusk offers a case statement that is similar to Delphi's, with one notable improvement. Instead of this…

var Status: TYesNo; case Status of ynUnknown: Writeln('Uncertain'); ynNo: Writeln('No'); ynYes: Writeln('Yes'); else InternalError; end;

You can do this…

var Status: TYesNo; case:strict Status of ynUnknown: Writeln('Uncertain'); ynNo: Writeln('No'); ynYes: Writeln('Yes'); end;

Tusk Feature

In Tusk, it is a parse-time error if a strict case statement does not have every choice from the underlying data type.


DSFormat Validation

The following code compiles fine in Delphi, but has some obvious errors…

Writeln(DSFormat('%d %b', [GetComputerName, Now]));

Above, the format specifiers don't match the data types of the two values. Delphi finds these issues at runtime, but Tusk catches them at parse time. Tusk also detects when there are too few arguments, as in the following…

Writeln(DSFormat('%s %b', [GetComputerName]));

This validation only applies when the two arguments (the format string and the argument array) are both frozen (known at parse time).

Additionally, this logic applies to other global routines that wrap DSFormat, such as DSRaise, and to interface methods like IWriter.WritelnFmt.

Tusk Feature

In Tusk, calls to DSFormat and similar routines/methods are validated at parse time (when the arguments are frozen).


Call Suppression Operator

In Delphi, the @ operator serves two different purposes. Primarily, it is the address-of operator, but in addition to this, it is sometimes used to prevent calling a function. In Tusk, these are two different operators: @ is for taking the address; @@ prevents calling a function. For example…

function GetCount: Double; begin Result := Length(GetUserName) ** 2; end; var n := GetCount; // n is a Double var p := @n; // p is a ^Double var f := @@GetCount; // f is a function: Double

Above, p is a pointer to Double, n is a Double. We want f to be an alias to GetCount. Without the @@ operator, f would be the result of calling GetCount (this is how n is defined). However, with the @@ operator, the call to GetCount is suppressed, so f and GetCount refer to the same function.

Function Compatibility

Delphi has a very strict system for function compatibility: function types must match exactly. Tusk offers a more flexible system, with four main advantages over Delphi.

Functions as Procedures

Consider this Delphi code…

type TStrProc = reference to procedure( const Value: string); procedure StrProc(const Value: string); begin Writeln('<<', Value, '>>'); end; function StrFunc(const Value: string): Boolean; begin Writeln('<<', Value, '>>'); Result := Value <> ''; end; var sp: TStrProc; begin sp := StrProc; // This is fine; sp := StrFunc; // This is a compile error. end;

Above, Delphi will not allow StrFunc to be used as a TStrProc, even though, logically, it could (by simply discarding the result).

The above code works fine in Tusk, because…

Tusk Feature

In Tusk, a function converts to a procedure type if the parameters are compatible (discarding the function result).


Covariance on Return Type

Consider this Delphi code…

type TDoubleFunc = reference to function: Double; function DblFunc: Double; begin Result := 123.45; end; function IntFunc: Integer; begin Result := 12345; end; var df: TDoubleFunc; begin df := DblFunc; // This is fine; df := IntFunc; // This is a compile error. end;

Above, Delphi will not allow IntFunc to be used as a TDoubleFunc, even though, logically, any function that returns an Integer should work as a function returning Double.

The above code works fine in Tusk, because…

Tusk Feature

In Tusk, a function returning T is compatible with a function returning Q if T is compatible with Q.


Contravariance on Parameter Types

Consider this Delphi code…

type TIntegerProc = reference to procedure( Value: Integer); procedure IntProc(Value: Integer); begin Writeln('<<', Value, '>>'); end; procedure DblProc(Value: Double); begin Writeln('<<', Value, '>>'); end; var ip: TIntegerProc; begin ip := IntProc; // This is fine; ip := DblProc; // This is a compile error. end;

Above, Delphi will not allow DblProc to be used as a TIntProc, even though, logically, it could (by simply promoting the Integer to Double).

The above code works fine in Tusk, because…

Tusk Feature

In Tusk, a routine taking a parameter of type T (by value or const) is compatible with a routine taking a parameter of type Q (by value or const) if Q is compatible with T.


Note

The above feature does not apply to var or out parameters, which must match exactly when comparing routine types.


const Parameters

In Tusk, a const parameter is passed by value, but cannot be changed within the function itself. Whether a parameter is passed by value or by const affects the called function, but not the caller. Therefore, the following code works fine in Tusk…

type TIntProc = reference to procedure( x: Integer); procedure p1(y: Integer); begin end; procedure p2(const z: Integer); begin end; var ip: TIntProc; ip := p1; ip(3); ip := p2; // In Delphi, this would not compile ip(4);

The above code works fine in Tusk, because…

Tusk Feature

In Tusk, value and const parameters are equivalent, for purposes of type compatibility.


out Parameters

In Delphi, out parameters pass the argument by reference, like var parameters. The difference is that, for managed types, out parameters are cleared out immediately before the called function executes. For other data types, out and var behave identically.

Tusk Feature

In Tusk, out parameters are consistent: they are always initialized at the point of call, regardless of data type.


Default Parameters

Data Types

In Tusk, there are no restrictions on what type of parameters can have default values. In Delphi, only certain data types can have defaults — for example, Integer and string are supported, but not Variant, objects, static arrays, or records.

Tusk Feature

In Tusk, parameters may have a default value, regardless of data type.


Late Evaluation

In Delphi, default parameter values must be compile-time constants. In Tusk, default parameter values may be any expression (evaluated at the time the function declaration is encountered). For example…

1│ var x: Integer := 123; 2│ 3│ procedure Work(z: Integer = x); 4│ begin 5│ Writeln(z); 6│ end; 7│ 8│ Work(3); 9│ 10│ Work; 11│ 12│ x := 999; 13│ 14│ Work;

The above program prints…

3 123 123

Note that the default value is captured on line 3, so the change to x on line 12 doesn't affect the call to Work on line 14.

Tusk Feature

In Tusk, default parameter expressions are evaluated for each call of the routine.


Pure Functions

In Tusk, functions may be pure, meaning that they exhibit no side effects, and always return the same value for a given set of inputs. Examples of such functions include Pos and Sqrt. Pure functions can be used to make frozen constants (those whose values are known at parse time). For example…

type MyInt = Integer; // type declarations are always frozen const k = Sqrt(16); // k is frozen, because Sqrt is pure and 16 is frozen var j: Integer; // variables are not frozen const m = Sqrt(j); // m is not frozen, because j is not case j of 2: Writeln('two'); k: Writeln('four'); // k is allowed, because it's frozen end;

Some functions are always pure, and others are pure only if all their arguments are frozen (for example, Sqrt and Pos). Others, of course, are never pure (even if their arguments are frozen), including Inc and Dec (due to side effects) and Now, which can return a different value with each call.

Tusk Feature

In Tusk, functions may be pure, allowing them to be called at parse time (for example, Sqrt, Abs, and GetComputerName).


Function Results

Delphi has a few less-than-ideal behaviors related to function results.

Return Value Optimization

For managed types, Result may be bound to a local variable in the caller's scope. This can lead to a couple of odd issues: Result may have a non-zero initial value, and it may apply changes to the caller's variable before the function completes.

Tusk Feature

In Tusk, Result is never aliased to any other value.


Initial Value

For non-managed types, Result has no defined value. Typically, the compiler warns if you use Result before giving it a value, but this is not reliable. For example…

procedure Work(var z: Integer); begin end; function Compute: Integer; begin Work(Result); Writeln(Result); Result := 0; end;

Above, Compute writes a random number, but the code is warning-free.

Tusk addresses this issue in a simple manner:

Tusk Feature

In Tusk, Result is always zero-initialized.


Undefined Result

Delphi typically warns if a function doesn't assign a value to Result (but only for non-managed types, and never for records).

Tusk addresses this issue in a simple manner:

Tusk Feature

In Tusk, Result is always zero-initialized.


Properties

Non-Array Properties

In Tusk, a property (of an interface) can be used anywhere a value of that type would be expected. This includes taking the address of a property or passing a property by reference (to a var parameter).

For example…

// IEmployee has a read/write Name property var Emp: IEmployee; Emp.Name := 'Joe Cool'; Writeln(Emp.Name); var p: ^string := @Emp.Name; p^ := 'Jane Smith'; Writeln(p^); procedure ChangeStr(var s: string); begin s := 'Wilma Flitstone'; end; ChangeStr(Emp.Name);

The above works fine in Tusk, even though most of it wouldn't work in Delphi, because…

Tusk Feature

Tusk supports taking the address of a property, and passing properties by reference.


Array Properties

For array properties, the above discussion applies to individual elements of the array property…

var List := NewList([4,5,6]); Writeln(List.Items[1]); Writeln(List[2]); var p: PVariant := @List.Items[0]; p^ := 888; Writeln(p^); procedure ChangeVar(var v: Variant); begin v := 333; end; ChangeVar(List[1]);

Tusk Feature

Tusk supports taking the address of an element of an array property, and passing an element of an array property by reference.


Pointer Math

For certain array properties, Tusk supports pointer math…

var List: IVariantList := [4,5,6]; var p: ^Variant := @List.Items[0]; while InBounds(p) do begin Writeln(p^); Inc(p); end;

To enable pointer math on an array property, use the [TuskArray] attribute. For example, here's how IReadOnlyVector is exposed (in the unit Tusk_GenUtil)…

Tusk_IReadOnlyVector = class public Obj: IReadOnlyVector; function get_Items(i: Integer): Variant; [TuskArray('Count')] property Items[i: Integer]: Variant read get_Items; default; function get_First: Variant; function get_Last: Variant; property First: Variant read get_First; property Last: Variant read get_Last; end;

The [TuskArray] attribute is only supported on indexed properties with a single index parameter, which must be an integer type.

The [TuskArray] attribute takes an argument, as shown above, a string specifying the name of the property that indicates the element count for the array property. This argument defaults to "Count", so the above is actually written as follows…

[TuskArray] property Items[i: Integer]: Variant read get_Items; default;

The count property (whatever it is called) must also be an integer type, and may (as in this example) be defined in the class that exposes an ancestor type (Tusk_IReadOnlyVector, in this case).

Tusk Feature

Tusk supports pointer math on certain array properties, such as the Items property of IReadOnlyVector.


Any and NoCall Types

Tusk defines the Any type, which is like Variant, but works better: Any values are compatible with any type, and do not alter the value when copying it.

Type Compatibility

Tusk Feature

Any is compatible with all data types, while Variant has the same restrictions as in Delphi. The following types are compatible with Any but not with Variant: sets, functions, pointers, and records that don't offer conversions to Variant.


Dynamic Arrays

Tusk Feature

When assigning a dynamic array to an Any, Tusk avoids using a Variant Array, to avoid the accompanying performance and type restrictions.


Records

In Tusk, an Any or NoCall that holds a record (via the VarTValue custom variant) has access to the record's fields, properties, and methods…

var Bits: TBitVector; Bits.SetBit(3); Writeln(Bits.HasBit(2)); // prints False Writeln(Bits.HasBit(3)); // prints True var BitsVar: Any := Bits; Writeln(BitsVar.HasBit(2)); // prints False Writeln(BitsVar.HasBit(3)); // prints True

Above, note that BitsVar is a copy of Bits (records are value types, after all).

Tusk Feature

When assigning a record to an Any, Tusk bypasses the record's conversion operator. For example, when assigning a Decimal to Any, Tusk keeps the Decimal; when assigning a Decimal to Variant, Tusk uses the same custom Variant type you'd get in a Delphi program.


Interface Conversions

In Delphi, an explicit cast is required to convert Variant to an interface (other than IInterface) . We typically use TType.VarSupports and similar functions.

Tusk Feature

In Tusk, Any / NoCall values can be implicitly converted to any interface type (not just IInterface).


Anonymous Methods

In Delphi, an explicit cast is required to convert an anonymous method to Variant. We typically use TAnonMeth.ToVar to accomplish this.

Tusk Feature

In Tusk, anonymous methods can be implicitly converted to Any / NoCall.


Dot and Subscript Operators

In Tusk, expressions of type Any and NoCall support the dot operator (requiring the value to support IAssociation)…

var v: Any := Something; Writeln(v.x); // TType.VarAs<IAssociation>(v)['x']

Similarly, Any and NoCall expressions support the square bracket operator for IAssociation and IReadOnlyVector objects…

var v: Any := SomeAssociation; Writeln(v['y']); // TType.VarAs<IAssociation>(v)['y'] var w: Any := SomeList; Writeln(w[2]); // TType.VarAs<IReadOnlyVector>(w)[2]

Tusk Feature

Tusk allows the use of the dot operator on an Any or NoCall value that happens to hold an IAssociation, and square brackets on an Any or NoCall value that happens to hold an IReadOnlyVector or IAssociation.


NoCall Type

Tusk offers the built-in type NoCall, which is like Any, except for the following: when converting a function expression to NoCall, Tusk will not attempt to add empty parentheses. For example…

function f: string = 'hello'; var v: Variant := f; // v is 'hello' (implicit function call) var a: Any := f; // a is 'hello' (implicit function call) var n: NoCall := f; // n is the function f (no implicit call) Show(v); // prints: 'hello' Show(a); // prints: 'hello' Show(n); // prints: function f: string = 'hello' Writeln(v); // prints: hello Writeln(a); // prints: hello Writeln(n); // raises an exception

Above, the last call to Writeln raises an exception because there is no implicit conversion from interface to string (in Tusk, functions are interfaces).

IDotBag

Tusk offers a built-in type, IDotBag, which is assignment-compatible with IAssociation. IDotBag does not expose any properties or methods of IAssociation or its ancestor interfaces. Instead, IDotBag allows you to access the default array property of IAssociation using the dot or square bracket operators…

var s := 'z'; var a: IAssociation := Something; Writeln(a['x'] + a['y'] + a[s]); var b: IDotBag := a; Writeln(b.x + b.y + b[s]);

In this way, IDotBag is similar to Any / NoCall, but it is more restrictive because IDotBag is still an interface, so (unlike Any / NoCall) it cannot be used with arithmetic operators, doesn't implicitly convert to other types, etc.

Tusk Feature

Tusk includes the IDotBag data type, which is assignment compatible with IAssociation, but offers a cleaner notation for accessing named properties.


Anonymous Method To Interface

In Delphi, an explicit cast is required to convert an anonymous method to IInterface. We typically use TAnonMeth.ToIntf to accomplish this.

Tusk Feature

In Tusk, anonymous methods can be implicitly converted to IInterface.


Last Modified: 2/15 9:51:46 am
In this article (top)  View article's Sage markup
2/15 9:51:46 am