Sage
Tusk Language
Welcome to Sage
Volume (41%) Hide Volume
Topics
Overall Structure
Tusk simplifies Delphi's unit concept: Tusk code is not "in a unit", and a Tusk script is much simpler than a Delphi unit.

Global Identifiers

For example, consider this small Delphi unit:

unit MyUnit; interface uses SysUtils, DSDecimal; function GetNumber: Decimal; implementation function GetNumber: Decimal; begin if AppConfigExists then Result := AppConfig.Get('Number', 100) else Result := 100; end; end.

The Tusk equivalent of the above is much more concise…

function GetNumber: Decimal; begin if AppConfigExists then Result := AppConfig.Get('Number', 100m) else Result := 100m; end;

Tusk eliminates the unit, uses, interface, and implementation keywords, along with the final end. These simplifications are appropriate for a scripting language, but a related issue arises.

If a Tusk script doesn't specify which unit(s) it uses, then which global identifiers are "in scope" for the script? The answer is simple: all of them. In other words, a Tusk script behaves as if it used every available unit. This is convenient, in that a script can simply go about its business, without first using a dozen or more units.

However, this arrangement would lead to ambiguity if two or more Delphi units expose the same global identifier to Tusk. Tusk doesn't allow this situation: at most one unit can expose a global identifier with a given name. To avoid this, Tusk allows global identifiers to be exposed without contributing to the global namespace. In such case, a unit prefix is required.

For example, the unit DSThreadUtil defines the ITask interface, which is exposed to Tusk via the Tusk_DSThreadUtil unit. If another unit, say MyUnit, defined its own ITask, then when exposing MyUnit.ITask to Tusk (in the unit Tusk_MyUnit), then this ITask would be exposed with the Tusk.NoGlobal option, indicating that this identifier does not contribute to the global namespace. Thus, in a Tusk script, ITask and DSThreadUtil.ITask both refer to the type in DSThreadUtil, while the other type can be referenced only with the fully qualified name of MyUnit.ITask.

This notation is familiar to Delphi programmers, where it is used to resolve ambiguity among multiple units, or when the same name is used for both a local and a global.

Note

Tusk offers a convenient syntax that reduces the need for fully qualified names.


Global vs. Local

Delphi maintains a distinction between code in the body of a routine and code at the global level. For example…

unit Sample; interface implementation var x: Integer; procedure Work; begin var y: Integer; end; end.

Above, two variables are defined, x and y. x is defined outside of any routines, so it is at the global level. In contrast, y is defined inside the Work procedure, so it is at the local level.

The remainder of this article discusses the differences between Tusk and Delphi, related to global vs. local declarations.

Motivation

Tusk treats global and local declarations similarly in order to make it easier to move code from the global level into a function (or vice-versa).

For example, you might start out with a simple Tusk script, with a handful of lines of code, then later decide to move that code into a routine. Unlike in Delphi, Tusk doesn't require you to alter the code along the way.


Code at the Global Level

Tusk allows code outside of an routine…

Writeln('Hello, world!');

The above is a perfectly valid Tusk program. In Delphi, we'd need to expand this to…

program HelloWorld; {$APPTYPE CONSOLE} {$R *.res} uses SysUtils; begin Writeln('Hello, world!'); end.

In Delphi, executable code must appear inside a routine, or the begin / end block in the .dpr.

Here's another quick example to highlight the flexibility that Tusk offers…

Writeln('Working...'); procedure Cleanup(const Folder: string); begin // Lots and lots of code end; Cleanup('C:\Folder1'); Cleanup('C:\Folder2'); Writeln('Done');

Above, the two calls to Writeln and the two calls to Cleanup appear at the global level, which would not be allowed in Delphi.

Variable Declarations

In Delphi, global variable declarations can define multiple variables in one var block, but cannot initialize the variables. In contrast, local variable declarations can define only one variable, but it may be initialized (and the type may be inferred)…

unit Sample; interface implementation var x: Integer; y: string; procedure Work; begin var a: Integer := 4; var b := False; end; end.

Tusk merges these two modes in to one, using it for both global and local scenarios. In Tusk, a var block can define only one variable, but it may be initialized (and the type may be inferred).

Note

The above restriction does not apply to var blocks in a routine but outside the begin / end block. For example…

procedure Work; var a: Integer := 4; b := False; begin end;

Above, a and b share a var block, but can be initialized (with type inference) – because the var block is local to the Work routine, but outside the begin / end block.


Constant and Type Declarations

In Delphi, global const and type declarations can define multiple identifiers in one block. In contrast, local const declarations can define only one constant (and local type declarations are not allowed at all)…

unit Sample; interface implementation type Num1 = Integer; Num2 = Double; const x = 1; y = 2; procedure Work; begin const a = 3; const b = 4; end; end.

Tusk merges these two modes in to one, using it for both global and local scenarios. In Tusk, a const or type block can define multiple identifiers (and local type blocks are allowed)…

type Num1 = Integer; Num2 = Double; const x = 1; y = 2; procedure Work; begin type Num3 = Decimal; Num4 = Cardinal; const a = 3; b = 4; end; end.

Above, note how Work has a type clause within the begin / end block, which is not allowed in Delphi.

Also, not how both the local type and const blocks define multiple symbols, which is also not allowed in Delphi.

Caution

There is a slight ambiguity involved with allowing multiple declarations in the same type or const block. For example…

var a, b: Decimal; procedure Work; begin const c = 1; d = 2; b = c; end;

The way the above is formatted matches Tusk's interpretation. However, another interpretation is possible…

var a, b: Decimal; procedure Work; begin const c = 1; d = 2; b = c; end;

Above, the b = c line is intended to invoke the following method…

class operator Equal(const x: Decimal; y: Integer): Boolean;

In both Delphi and Tusk, an operator overload qualifies as a statement (because, in theory, the method might have side effects). This would not be allowed (in Delphi or Tusk) if b was defined as a Double, for example.

However, Tusk treats the above example as a single const block defining c, d, and b. This is because a const or type block continues as long as possible.

To clarify our intentions here, we can introduce another statement between the const block and our operator overload…

1│ var a, b: Decimal; 2│ 3│ procedure Work; 4│ begin 5│ const 6│ c = 1; 7│ d = 2; 8│ ; 9│ b = c; 10│ end;

The null statement (line 8) terminates the const block. An empty begin / end block would also suffice.

Another, probably preferable option is to parenthesize the operator overload…

var a, b: Decimal; procedure Work; begin const c = 1; d = 2; (b = c); end;

This is only necessary immediately after a const or type block, and then only when you want to invoke an operator overload and discard the result. Tusk's pretty printer will remember the extra parens, by the way.

Overall, this issue should be extremely rare (or even non-existent), but it's worth mentioning just in case.


Redefining Symbols

In Delphi, inline variables and constants cannot redefine a symbol in the current scope. This sounds simple, but the rules are actually rather involved. For example, consider this Delphi code…

const x = 1; procedure Work; begin Writeln(x); const x = 2; Writeln(x); end;

The above prints 1 followed by 2, because the inline declaration of x redefines the global one.

Here's another example…

procedure Work2; begin var a := 4; if a > 2 then begin const x = 1; Writeln(x); end; if a > 3 then begin const x = 2; Writeln(x); end; end;

The above also prints 1 followed by 2, but here there are two versions of x in the same routine, though their lifetimes don't overlap.

Given the above two examples, it is rather odd that Delphi doesn't allow this…

procedure Work3; begin const x = 1; Writeln(x); const x = 2; Writeln(x); end;

Tusk simplifies things: a type, const, or var declaration may hide an existing symbol, even in the same scope. This simplifies things for both you and Tusk, and makes it easier for you to move things around.

Motivation

If this seems too loose or forgiving, remember that Delphi allows this…

const x = 1; procedure Work4; begin Writeln(x); // prints 1 const x = 2; // redefines x! Writeln(x); // prints 2 end;

but not this…

procedure Work5; begin const x = 1; Writeln(x); // prints 1 const x = 2; // redefines x! Writeln(x); // prints 2 end;

In both cases, the const x = 2 declaration redefines an existing definition of x – one that was used on the previous line of code (in the call to Writeln). Why does it matter whether the existing definition of x is in the same scope or not? Either way, we're still redefining a symbol we used on the previous line of code!

Tusk removes this restriction in the name of simplicity.


Overloaded Routines

Tusk has slightly different rules for overloading, compared to Delphi.

Overload After Non-Overload

In Delphi, if an overloaded routine follows a non-overloaded routine of the same name (in the same scope), a compile time error results…

procedure p(x: Integer); begin Writeln('Integer'); end; procedure p(x: Double); overload; begin Writeln('Double'); end;

In Tusk, the above is legal; the second definition of p overloads the first — as if both were marked with overload.

Non-Overload After Overload

In Delphi, if a non-overloaded routine follows an overloaded routine of the same name (in the same scope), a compile time error results…

procedure p(x: Integer); overload; begin Writeln('Integer'); end; procedure p(x: Double); begin Writeln('Double'); end;

In Tusk, the above code is legal; the second definition of p replaces the first. This would also be the case if neither routine were overloaded: a non-overloaded function declaration always replaces any existing identifier (regardless of type).

Mixing Scopes

In Tusk, a procedure or function declaration may appear in the body of an enclosing routine…

procedure p(x: Double); overload; begin Writeln('Double'); end; procedure Work; begin p(4); p(5.6); procedure p(x: Integer); overload; begin Writeln('Integer'); end; p(4); p(5.6); end;

Above, note how the Double overload of p is declared in the body of the Work procedure. Though not allowed in Delphi, Tusk permits this because Tusk treats global and local declarations similarly.

Therefore, the above code prints…

Double Double Integer Double

Note that p becomes overloaded after the first two calls. Of course, outside of Work, p is not overloaded.

The same behavior is maintained if the local routine is moved before the begin / end block…

procedure p(x: Integer); overload; begin Writeln('Integer'); end; procedure Work; procedure p(x: Double); overload; begin Writeln('Double'); end; begin p(4); p(5.6); end;

This last example prints "Integer" then "Double". It also compiles in Delphi, but with different behavior: it prints "Double" then "Double". Again, this is all down to Tusk blurring the distinction between global and local declarations.

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