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.
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.
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.
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.
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).
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.
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.
Tusk has slightly different rules for overloading,
compared to Delphi.
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.
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).
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