Functions and Other Global Declarations

In this chapter we detail the different entities that can appear in the global scope of a KL program, including functions and function-like entities, name constants, and instances of the require statement. Structure and object definitions can also appear in the global scope and are covered in the section The KL Type System.

Functions

A function is a collection of program statements that can be called from another part of a program. A function takes a list of zero or more parameters and optionally returns a return value.

Function Definitions

Function definitions in KL are much the same as the “traditional” function definition syntax in JavaScript, with the following key differences:

  • The return type and the type of each function parameter must be explicitly declared. If a function does not return a value, the return type must be omitted.
  • The parameter declarations may additionally declare the parameter as input (read-only; the default) by preceding the type by in or input-output (read-write) by preceding the value by io.
  • Functions can optionally be defined using the inline keyword in place of function; see Inline Functions and Methods.
  • The function keyword is optional. If neither function nor inline is present then function is assumed.
/*
** Example: Function Definitions
*/

// Function returning a value and using only
// input parameters

function Float32 add(Float32 lhs, Float32 rhs) {
  return lhs + rhs;
}

// Function not returning a value and using both
// input and input-output parameters

function add(in Float32 lhs, in Float32 rhs, io Float32 result) {
  result = lhs + rhs;
}

// The 'function' keyword is totally optional
Float32 double(Float32 x) {
  return 2 * x;
}

operator entry() {
  report(add(2, 2));

  Float32 addResult;
  add(2, 2, addResult);
  report(addResult);

  report(double(2));
}

/*
** Output:

+4.0
+4.0
+4.0

*/

Function Invocations

Function invocations (“calls”) are made using the same syntax as JavaScript, namely by appending a comma-delimited list of arguments, surrounded by parentheses, to the function name.

/*
** Example: Function Invocation
*/

function Integer add(Integer lhs, Integer rhs) {
  return lhs + rhs;
}

operator entry() {
  report("2 plus 2 is " + add(2, 2));
}

/*
** Output:

2 plus 2 is 4

*/

Function Prototypes

A function prototype in KL is a function declaration that is missing a body. Providing a function prototype allows the function to be called before it is defined. This is useful under two circumstances:

  • When two or more functions call each other. Such functions are sometimes referred to as co-recursive:

    /*
    ** Example: Co-recursive Functions
    */
    
    // Function prototype for 'two', so that 'one' can call it before it is defined
    function two(Integer n);
    
    // The function 'one' calls 'two' even though it is not yet defined
    function one(Integer n) {
      report("one");
      if (n > 0)
        two(n - 1);
    }
    
    // The definition of the function 'two' comes after its prototype
    function two(Integer n) {
      report("two");
      if (n > 0)
        one(n - 1);
    }
    
    operator entry() {
      one(4);
    }
    
    /*
    ** Output:
    
    one
    two
    one
    two
    one
    
    */
    
  • When a function definition is provided by a Fabric extension. The name of the symbol of the function in the Fabric extension is provided by appending = "symbol name" or = 'symbol name' to the function prototype. These is usually referred to as :defn:`external functions`:

    /*
    ** Example: External Functions
    */
    
    // The prototype 'libc_perror' is linked to an external function 'perror'
    function libc_perror(Data cString) = 'perror';
    
    // The KL function 'perror' is what KL functions actually call
    function perror(String string) {
      libc_perror(string.data());
    }
    
    operator entry() {
      perror("something that caused an error");
    }
    

Polymorphism

KL supports compile-time function polymorphism. This means that you can have multiple functions with the same name so long as they have a different number of parameters or those parameters differ by type and/or their input versus input-output qualification.

Note

It is an error to have two functions with the same name that take exactly the same parameter types but return different types

When a function call is made in KL source, if there are multiple functions with the same name then the KL compiler uses a best-match system to determine which function to call. Exact parameter type matches are always prioritized over type casts. If the compiler is unable to choose a unique best match then an error will be reported showing the ambiguity.

The following example demonstrates a simple use of function polymorphism:

/*
** Example: Function Polymorphism
*/

function display(Integer a) {
  report("integer value is " + a);
}

function display(String s) {
  report("string value is '" + s + "'");
}

operator entry() {
  Integer integer = 42;
  display(integer);

  String string = "hello";
  display(string);

  Byte byte = 64;
  display(byte);
}

/*
** Output:

integer value is 42
string value is 'hello'
integer value is 64

*/

Operators

The operator keyword in KL is used to mark functions that are to be used as entry points into KL from the Fabric dependency graph. Operators are declared in the same way as functions except that they must not return a value. Fabric does special type-checking to ensure that operators are bound properly to nodes in a Fabric dependency graph.

operator addElements(io Float32 lhs, io Float32 rhs, io Float32 result) {
  result = lhs + rhs;
}

Constructors

A constructor for a user-defined type is a function that initializes a value with the given the type from other values.

Constructor Declarations

A constructor is declared as a function whose name is the name of the user-defined type. The function can take any number of parameters, all of which must be input parameters; constructors cannot take input-output parameters. Constructors cannot return values.

Within the body of a constructor definition, the value being initialized is referred to with the this keyword; its members are accessed using the . (dot) operator. In this context, this is always read-write, ie. its members can be modified.

/*
** Example: Constructor Declarations
*/

struct Complex32 {
  Float32 re;
  Float32 im;
};

// The empty constructor;
function Complex32() {
  this.re = this.im = 0.0;
}

// Construct a Complex from a Float32
function Complex32(Float32 x) {
  this.re = x;
  this.im = 0.0;
}

// Construct a Complex from two Float32s
function Complex32(Float32 x, Float32 y) {
  this.re = x;
  this.im = y;
}

operator entry() {
  report(Complex32());
  report(Complex32(3.141));
  report(Complex32(3.141, 2.718));
}

/*
** Output:

{re:+0.0,im:+0.0}
{re:+3.141,im:+0.0}
{re:+3.141,im:+2.718}

*/

Like functions, constructors can optionally be defined using the inline keyword in place of function; see Inline Functions and Methods.

Constructor Invocation

Constructors are invoked in one of several ways.

Naked Initialization

If a variable is declared without any initialization, the empty constructor (ie. the constructor that takes no parameters) is invoked to initialize the variable. This is referred to as naked initialization.

/*
** Example: Naked Initialization
*/

struct MyType {
  Integer n;
  Float32 x;
};

// The empty constructor
function MyType() {
  this.n = 42;
  this.x = 3.141;
}

operator entry() {
  MyType myType; // invokes the empty constructor
  report(myType);
}

/*
** Output:

{n:42,x:+3.141}

*/

Assignment Initialization

If a variable is assigned to as part of its declaration, a single-parameter constructor is invoked. This is referred to as assignment initialization. If there isn’t an exact match for the type of the value assigned, best-match polymorphism rules are used to choose the constructor to invoke.

Example:

struct MyType {
  String string;
};

// Construct from a string
function MyType(String string) {
  this.string = "The string was '" + string + "'";
}

// Construct from a scalar
function MyType(Float64 float64) {
  this.string = "The float64 was " + float64;
}

operator entry() {
  // Construct MyType from String value
  MyType myTypeFromString = "foo";
  report(myTypeFromString);

  // Construct MyType from Float64 value
  MyType myTypeFromFloat64 = 2.718;
  report(myTypeFromFloat64);

  // There is no constructor that takes a Boolean but
  // there is a cast from Boolean to String
  MyType myTypeFromBoolean = true;
  report(myTypeFromBoolean);
}

Output:

{string:"The string was 'foo'"}
{string:"The float64 was 2.718"}
{string:"The string was 'true'"}

Invocation Initialization

If a variable is “called” (ie. using function call syntax) as part of its declaration, the constructor taking the given arguments is invoked. This is referred to as invocation initialization. If there isn’t an exact match for the arguments passed to the call, best-match polymorphism rules are used to choose the constructor to invoke.

Example:

struct Vec2 {
  Float64 x;
  Float64 y;
};

// Construct from two scalars
function Vec2(Float64 x, Float64 y) {
  this.x = x;
  this.y = y;
}

operator entry() {
  Vec2 vec2FromFloat64s(3.141, 2.718);
  report(vec2FromFloat64s);
  Vec2 vec2FromIntegers(42, -7);  // Uses best-match polymorphism to convert Integer to Float64
  report(vec2FromIntegers);
}

Output:

{x:3.141,y:2.718}
{x:42,y:-7}

Temporary Initialization

If a function call is performed where the name of the function is the name of the type, the constructor taking the given arguments is invoked to create a temporary value of the named type. If there isn’t an exact match for the arguments passed to the call, best-match polymorphism rules are used to choose the constructor to invoke. This is refered to as temporary initialization.

Note

KL does not distinguish between construction and casting. Casting a value to a different type is the same as constructing a temporary value of the given type and initializing it, using the appropriate constructor, from the given value.

Example:

struct Vec2 {
  Float64 x;
  Float64 y;
};

// Construct from two scalars
function Vec2(Float64 x, Float64 y) {
  this.x = x;
  this.y = y;
}

operator entry() {
  report(Vec2(3.141, 2.718));
  report(Vec2(42, -7));  // Uses best-match polymorphism to convert Integer to Float64
}

Output:

{x:3.141,y:2.718}
{x:42,y:-7}

Base type constructors (inheritance)

New in version 1.13.0.

When a specialized structure or object type inherits from a base type, the base type’s default constructor is implicitly called before the specialized type’s one.

Note

It is a current limitation that base type constructors with arguments cannot be called by specialized type constructors. The following example uses an initialize method to workaround this issue:

object Shape {
  Float32 centerX, centerY;
};

inline Shape( Float32 centerX, Float32 centerY ) {
  this.initialize( centerX, centerY );
}

/// \internal
inline Shape.initialize!( Float32 centerX, Float32 centerY ) {
  this.centerX = centerX;
  this.centerY = centerY;
}

object Circle : Shape {
  Float32 radius;
};

inline Circle( Float32 centerX, Float32 centerY, Float32 radius ) {
  this.parent.initialize( centerX, centerY );
  this.radius = radius;
}

operator entry() {
  Circle c( 1, 2, 3);
  report( c );
}

/*
** Output:

{centerX:+1.0,centerY:+2.0,radius:+3.0}

*/

Destructors

A destructor is a function that is called when a variable goes out of scope and its resources are freed. Destructors are declared by prepending ~ (tilde) in front of the name of the type and using it as a function. Destructors cannot take any parameters or return values. The destructor is called before the value is freed so that its members are still accessible. In the body of the destructor the value is referred to using the this keyword; the value is input-output, ie. it can be modified in the destructor.

Example use of destructor:

struct MyType {
  String s;
};

// Empty constructor
function MyType() {
  this.s = "foo";
  report("Creating MyType: this.s = " + this.s);
}

// Destructor
function ~MyType() {
  report("Destroying MyType: this.s = " + this.s);
}

operator entry() {
  MyType myType;
}

Output:

Creating MyType: this.s = foo
Destroying MyType: this.s = foo

Like functions, destructors can optionally be defined using the inline keyword in place of function; see Inline Functions and Methods.

When a specialized structure or object type inherits from a base type, base type’s destructor is called after the specialized one.

Methods

A method is a function that operates on a user-defined structure. It uses a slightly different (and more suggestive) syntax than plain function calls for the case that the method call is strongly tied to a value whose type is a user-defined structure.

Method Definitions

If Type is a structure or alias, then a method named methodName can be added to the type using the following syntax:

// A method that returns a value
function <ReturnType> <Type>.<methodName>(<parameter list>) {
  <method body>
}

// A method that does not return a value
function <Type>.<methodName>(<parameter list>) {
  <method body>
}

Within the method body, this refers to the value on which the method is called. this is read-only if the method returns a value and is read-write if the method does not return a value.

Like functions, methods can optionally be defined using the inline keyword in place of function; see Inline Functions and Methods.

Method Invocation

If value is a value of type Type then the method methodName can be invoked on value using the expression value.methodName(argument list).

Just as there can be multiple functions with the same name, a given type can have multiple methods with the same name. When deciding which method to invoke, the usual best-match rules apply.

Example of method definition and invocation:

/*
** Example: Method Definition and Invocation
*/

struct MyType {
  Integer a;
  Float32 b;
};

// Add method desc to MyType
function String MyType.desc() {
  return "a:" + this.a + "; b:" + this.b;
}

operator entry() {
  MyType t;
  t.a = 1;
  t.b = 3.14;
  // Reports 'a:1; b:3.14'
  report(t.desc());
}

/*
** Output:

a:1; b:+3.14

*/

Methods Taking Read-Only or Read-Write Values for this

Changed in version 1.12.0: this now always defaults to read-only in method definitions unless an explicit ! is specified after method name; the default no longer depends on whether the method returns a value.

Whether this is read-only or read-write (in compiler terms, an r-value or an l-value) can be controlled on a per-method basis. By default, this is read-only; this can be made read-write by suffixing the method name with ! (exclamation mark). The method name can be suffixed with ? (question mark) to explicitly mark read-only methods.

Example of explicit read-only or read-write this in methods:

/*
** Example: Explicit read-only or read-write "this" in methods
*/

struct Vec2 {
  Float64 x;
  Float64 y;
};

function Vec2(in Float64 x, in Float64 y) {
  this.x = x;
  this.y = y;
}

// Explicitly make 'this' read-only
function Vec2.getComponents?(io Float64 x, io Float64 y) {
  x = this.x;
  y = this.y;
}

function Float64 Vec2.normSq() {
  return this.x*this.x + this.y*this.y;
}

function Float64 Vec2.norm() {
  return sqrt(this.normSq());
}

function Vec2./=(in Float64 value) {
  this.x /= value;
  this.y /= value;
}

// Explicitly make 'this' read-write
function Float64 Vec2.normalizeAndReturnOldNorm!() {
  Float64 oldNorm = this.norm();
  this /= oldNorm;
  return oldNorm;
}

operator entry() {
  Vec2 vec2(3.14, 2.71);

  Float64 x, y;
  vec2.getComponents(x, y);
  report("vec2.getComponents: x=" + x + ", y=" + y);

  report("vec2.normalizeAndReturnOldNorm returned " + vec2.normalizeAndReturnOldNorm());
  report("vec2 is now " + vec2);
}

/*
** Output:

vec2.getComponents: x=+3.14, y=+2.71
vec2.normalizeAndReturnOldNorm returned +4.147734321289154
vec2 is now {x:+0.757039809392627,y:+0.653368752692363}

*/

Interface methods and inheritance

New in version 1.13.0.

Although it is usually transparent to the KL coder, interface method’s calling mechanism differs from usual methods, and this requires special care in some situations.

A specialized object can inherit from a base object type. If that base type implements an interface, the specialized object can provide its own implementation of the same interface methods. In that case, invoking the interface method will always call the specialized version of the method (the specialized object method overrides the base object method). This is always true, and it doesn’t matter if the method is called in the context of functions, specialized object’s methods, or base object’s method.

However, it is frequent that the specialized implementation of a method needs to invoke its base implementation. The Type.parent.methodName syntax allows a specialized class to invoke the base implementation of an interface method, as seen below:

interface Described {
  String describe();
};

object Shape : Described {
  Float32 centerX, centerY;
};

function String Shape.describe() {
  return "Center: (" + this.centerX + ", " + this.centerY + ")";
}

object Circle : Shape {
  Float32 radius;
};

inline Circle.setRadius!( Float32 r ) {
  this.radius = r;
}

function String Circle.describe() {
  // Call Shape.describe and append to it
  return this.parent.describe() + " Radius: " + this.radius;
}

operator entry() {
  Circle c();
  c.centerX = 1;
  c.centerY = 2;
  c.radius = 3;

  Described d = c;
  report( d.describe() );
}

/*
** Output:

Center: (+1.0, +2.0) Radius: +3.0

*/

Access to Methods

New in version 1.15.0.

Access to methods can be controlled in the same was as access to members using the public, private and protected keywords:

/*
** Example: Access to Methods
*/

interface Int
{
  private int_priv();
  protected int_prot();
};

object A : Int
{
};

public A.a_pub()
{
  this.int_priv(); // ok since A implements Int
  this.int_prot(); // ok since A implements Int
}

protected A.a_prot() {}
private A.a_priv() {}

A.int_priv() {}
A.int_prot() {}

object B : A
{
};

public A.b_pub()
{
  this.int_priv(); // error since int_priv is private
  this.int_prot(); // ok since B inherits A
}

protected B.b_prot()
{
  this.a_prot(); // ok since B inherits A
}

private B.b_priv()
{
  this.a_priv(); // error since a_priv is private
}

operator entry()
{
  A a();
  a.a_pub(); // ok since a_pub is public
  a.a_prot(); // error since a_prot is protected
  a.a_priv(); // error since a_prot is private

  B b();
  b.b_pub(); // ok since a_pub is public
  b.b_prot(); // error since a_prot is protected
  b.b_priv(); // error since a_prot is private
}

/*
** Output:
(stdin):52:3: error: cannot access protected function A.a_prot?()
(stdin):53:3: error: cannot access private function A.a_priv?()
(stdin):57:3: error: cannot access protected function B.b_prot?()
(stdin):58:3: error: cannot access private function B.b_priv?()
(stdin):45:3: error: cannot access private function A.a_priv?()


*/

Overloaded Operators

KL allows overloading of binary operators and compound assignment operators for custom types (ie. specified through struct).

Like functions, operator overloads can optionally be defined using the inline keyword in place of function; see Inline Functions and Methods.

Binary Operator Overloads

Binary operators can be overloaded using the following syntax:

/*
** Example: Binary Operator Overloads
*/

struct MyType {
  Integer a;
  Float32 b;
};

function MyType +(MyType lhs, MyType rhs) {
  MyType result;
  result.a = lhs.a + rhs.a;
  result.b = lhs.b + rhs.b;
  return result;
}

operator entry() {
  MyType t1; t1.a = 42; t1.b = 3.14; report(t1);
  MyType t2; t2.a = 7; t2.b = 2.72; report(t2);
  MyType t3 = t1 + t2; report(t3);
}

/*
** Output:

{a:42,b:+3.14}
{a:7,b:+2.72}
{a:49,b:+5.86}

*/

Any of the binary arithmetic (+, -, *, / and %), bitwise (|, &, ^, << and >>) and comparison (==, !=, <, <=, > and >=) operators can be overloaded.

Binary operator overloads are subject to the following restrictions:

  • They must take exactly two parameters. The two parameters may be of any type and the two types may be different but they must both be input-only parameters.
  • They must return a value. However, the return type can be any type.

Unary Operator Overloads

New in version 1.12.0: Unary Operator Overloads

Unary operators can be overloaded using the following syntax:

/*
** Example: Binary Operator Overloads
*/

struct MyType {
  Integer a;
  Float32 b;
};

function MyType -MyType() {
  MyType result;
  result.a = -this.a;
  result.b = -this.b;
  return result;
}

operator entry() {
  MyType t1; t1.a = 42; t1.b = 3.14; report(-t1);
  MyType t2; t2.a = 7; t2.b = 2.72; report(-t2);
}

/*
** Output:

{a:-42,b:-3.14}
{a:-7,b:-2.72}

*/

Only the unary operators +, - and ~ can be overloaded.

Unary operator overloads are subject to the following restrictions:

  • They must return a value. However, the return type can be any type.

Direct Assignment Overloads

KL provides a default direct assignment for custom types which simply assigns each of the members. However, it is also possible to provide an overload for the direct assignment operator as shown in the example below:

.. kl-example:: Direct Assignment Overload
struct A {
UInt32 a;

};

A(UInt32 x) {
this.a = x;

}

A.=(A a) {
report(“Performing assignment”); this.a = 2 * a.a;

}

operator entry() {
A a1(42), a2(56); report(“Before: a1 = ” + a1 + ”, a2 = ” + a2); a1 = a2; report(“After: a1 = ” + a1 + ”, a2 = ” + a2);

}

Compound assignment overloads are subject to the following restrictions:

  • They must take exactly one parameter. The parameter may be of any type but it must be an input-only parameter.
  • They must not return a value.

Compound Assignment Overloads

KL provides a default direct assignment for custom types which simply assigns each of the members. It also provides a default compound assignment operator (ie. +=, -=, *=, /=, %=, |=, &=, ^=, <<= and >>=) by composing the associated binary operator, if available, with an assignment.

However, it is also possible to provide an overload for any of the compound assignment operators using the following syntax:

struct Type {
  Integer a;
  Float32 b;
};

function Type.+=(Type that) {
  this.a += that.a;
  this.b += that.b;
}

operator entry() {
  Type t1; t1.a = 42; t1.b = 3.14; report("t1 is " + t1);
  Type t2; t2.a = 7; t2.b = 2.72; report("t2 is " + t2);
  t1 += t2; report("t1 is now " + t1);
}

This produces the following output:

t1 is {a:42,b:3.14}
t2 is {a:7,b:2.72}
t1 is now {a:49,b:5.86}

Compound assignment overloads are subject to the following restrictions:

  • They must take exactly one parameter. The parameter may be of any type but it must be an input-only parameter.
  • They must not return a value.

Inline Functions and Methods

Functions, methods, and so on–but not operators–can optionally be declared with the inline keyword in place of the function keyword, which tells KL to try to inline the function definition wherever it is used. inline should generally only be used on small functions, which this may result in improved runtime performance:

inline Integer add(Integer lhs, Integer rhs) {
  return lhs + rhs;
}

Built-In Functions and Methods

KL has several built-in functions and methods that are available to all KL programs.

Debugging Functions

function report(String message)

Outputs a message to wherever messages are sent from KL; when Fabric Engine is used from the command line or when the KL tool is used the output is sent to standard error and standard output respectively. A newline is appended to the message when it is sent.

Within Fabric Engine the report function is primarily used for debugging, whereas it is used for general output from the KL tool.

function dumpstack()

New in version 1.13.0.

Outputs the KL function call stack that leads to the calling location, including KL file names and line numbers. For example the following KL code:

function func2()
{
  dumpstack();
}

function func1()
{
  func2();
}

operator entry()
{
  func1();
}

Will output:

1 function.func2() call.kl:4
2 function.func1() call.kl:9
3 operator.entry() call.kl:14
4 kl.internal.entry.stub.cpu()

Error Status Functions

KL maintains a contextual error status which can be set, queried and reset using some built-in functions. This status is restricted to the contextual KL evaluation and thread. Some KL operations such as integer divide-by-zero and array out-of-bounds access (when running KL with bounds checking enabled) will internally call setError. Fabric Engine extensions typically set the error status as a way to report operation failures.

function String getLastError()

Get the last error status that was set.

function clearLastError()

Resets the last error status.

function setError(String status)

Sets a new error status and reports it using the report mechanism.

Integer Numerical Functions

KL has support for several integer numerical functions that are helpful when dealing with integer expressions. Each of these functions has a version for each of the numerical types (UInt8, SInt8; UInt16, SInt16; UInt32, SInt32; UInt64, SInt64). The one that is called is chosen using polymorphism best-match rules; see Polymorphism.

function <SignedIntegerType> abs(<IntegerType> n)

Returns the integer absolute value of the argument.

Regardless of the type of the argument n, the type of the return value is signed, and is the absolute value of the argument n interpreted as a signed integer. This allows the abs function to be used on expressions involving differences of unsigned integers, eg. abs(Size(offset)-Size(index))

Floating-Point Numerical Functions

KL has support for many of the “standard library” floating-point numerical functions from C. Each of these functions has a version that takes a parameter or parameters of type Float32, and another that takes a parameter or parameters of type Float64. The one that is called is chosen using polymorphism best-match rules; see Polymorphism.

Trigonometric Functions

Like the C standard library, all trigonometric function use radians for their arguments and return values, where appropriate.

function Float32 sin(Float32 x)
function Float64 sin(Float64 x)

Returns the sine of the angle x. x is measured in radians.

function Float32 cos(Float32 x)
function Float64 cos(Float64 x)

Returns the cosine of the angle x. x is measured in radians.

function Float32 tan(Float32 x)
function Float64 tan(Float64 x)

Returns the tangent of the angle x. x is measured in radians.

function Float32 asin(Float32 x)
function Float64 asin(Float64 x)

Returns the arcsine of the argument x. The return value is measured in radians.

function Float32 acos(Float32 x)
function Float64 acos(Float64 x)

Returns the arccosine of the argument x. The return value is measured in radians.

function Float32 atan(Float32 x)
function Float64 atan(Float64 x)

Returns the arctangent of the argument x. The return value is measured in radians.

Warning

This function doesn’t work for large x and can only return values in the range ; use the atan2 function instead when possible.

function Float32 atan2(Float32 y, Float32 x)
function Float64 atan2(Float64 y, Float64 x)

Returns the arctangent of the ratio y/x; the result is measured in radians and is in the range .

Exponential and Logarithmic Functions

function Float32 pow(Float32 x, Float32 y)
function Float64 pow(Float64 x, Float64 y)

Returns the value of x raised to the power of y.

function Float32 pow(Float32 x, <IntegerType> y)
function Float64 pow(Float64 x, <IntegerType> y)

Returns the value of x raised to the power of y where y is an integer. Uses exponentiation by squaring for very high performance, and will expand into a fixed operation in the case that y is a constant integer.

function Float32 exp(Float32 x)
function Float64 exp(Float64 x)

Returns the value of raised to the power of x where is the base of the natural logarithm (approximately 2.7182818...).

function Float32 log(Float32 x)
function Float64 log(Float64 x)

Returns the natural (base ) logarithm of x.

function Float32 log10(Float32 x)
function Float64 log10(Float64 x)

Returns the common (base 10) logarithm of x.

Non-Transcendental Functions

function Float32 abs(Float32 x)
function Float64 abs(Float64 x)

Returns the absolute value of x.

function Float32 round(Float32 x)
function Float64 round(Float64 x)

Returns the value of x rounded to the nearest whole (fractional part of zero) floating-point number.

function Float32 floor(Float32 x)
function Float64 floor(Float64 x)

Returns the greatest whole floating-point number less than or equal to x.

function Float32 ceil(Float32 x)
function Float64 ceil(Float64 x)

Returns the smallest whole floating-point number greater than or equal to x.

Category Functions

function Boolean Float32.isReg()
function Boolean Float64.isReg()

Returns true if and only if the floating-point number is a regular floating-point number; that is, if it is not infinite and not a NaN (not-a-number) value.

function Boolean Float32.isInf()
function Boolean Float64.isInf()

Returns true if and only if the floating-point number is infinite. Note that this does not check for NaN values; use the Float32.isNaN() method for that.

function Boolean Float32.isNaN()
function Boolean Float64.isNaN()

Returns true if and only if the floating-point number is a not-a-number (NaN) value. Note that this does not check for infinite values; use the Float32.isInf() method for that.

Note

For a floating-point value x, the condition !x.isReg() is equivalent to x.isInf() || x.isNaN()

Vector Functions

KL support a large set of vector functions that are automatically made available for structures whose members are all of the same integer or floating-point type (as is usually the case for structures that represent vectors). The KL compiler automatically reduces the function call to vector intrinsic operation that is optimal for the running architecture; for example, on a modern Intel x86 machine they will be reduced to instructions using the SSE or AVX vector extensions, resulting in improved performance over non-vector code.

When <V> is a structure whose members <m1>, <m2>, ... <mN> are all of exactly the same integer or floating-point type <T>, the following functions are made available:

function <V> vecAdd(<V> lhs, <V> rhs)

Returns lhs.m1 + rhs.m1, lhs.m2 + rhs.m2, ... lhs.mN + rhs.mN

function <V> vecAdd(<T> k, <V> rhs)

Returns k + rhs.m1, k + rhs.m2, ... k + rhs.mN

function <V> vecAdd(<V> lhs, <T> k)

Returns lhs.m1 + k, lhs.m2 + k, ... lhs.mN + k

function <V> vecSub(<V> lhs, <V> rhs)

Returns lhs.m1 - rhs.m1, lhs.m2 - rhs.m2, ... lhs.mN - rhs.mN

function <V> vecSub(<T> k, <V> rhs)

Returns k - rhs.m1, k - rhs.m2, ... k - rhs.mN

function <V> vecSub(<V> lhs, <T> k)

Returns lhs.m1 - k, lhs.m2 - k, ... lhs.mN - k

function <V> vecMul(<V> lhs, <V> rhs)

Returns lhs.m1 * rhs.m1, lhs.m2 * rhs.m2, ... lhs.mN * rhs.mN

function <V> vecMul(<T> k, <V> rhs)

Returns k * rhs.m1, k * rhs.m2, ... k * rhs.mN

function <V> vecMul(<V> lhs, <T> k)

Returns lhs.m1 * k, lhs.m2 * k, ... lhs.mN * k

function <V> vecDiv(<V> lhs, <V> rhs)

Returns lhs.m1 / rhs.m1, lhs.m2 / rhs.m2, ... lhs.mN / rhs.mN

function <V> vecDiv(<T> k, <V> rhs)

Returns k / rhs.m1, k / rhs.m2, ... k / rhs.mN

function <V> vecDiv(<V> lhs, <T> k)

Returns lhs.m1 / k, lhs.m2 / k, ... lhs.mN / k

function <V> vecRem(<V> lhs, <V> rhs)

Returns lhs.m1 % rhs.m1, lhs.m2 % rhs.m2, ... lhs.mN % rhs.mN

function <V> vecRem(<T> k, <V> rhs)

Returns k % rhs.m1, k % rhs.m2, ... k % rhs.mN

function <V> vecRem(<V> lhs, <T> k)

Returns lhs.m1 % k, lhs.m2 % k, ... lhs.mN % k

When <T> is an integer type, the following additional function are available:

function <V> vecBitOr(<V> lhs, <V> rhs)

Returns lhs.m1 | rhs.m1, lhs.m2 | rhs.m2, ... lhs.mN | rhs.mN

function <V> vecBitOr(<T> k, <V> rhs)

Returns k | rhs.m1, k | rhs.m2, ... k | rhs.mN

function <V> vecBitOr(<V> lhs, <T> k)

Returns lhs.m1 | k, lhs.m2 | k, ... lhs.mN | k

function <V> vecBitAnd(<V> lhs, <V> rhs)

Returns lhs.m1 & rhs.m1, lhs.m2 & rhs.m2, ... lhs.mN & rhs.mN

function <V> vecBitAnd(<T> k, <V> rhs)

Returns k & rhs.m1, k & rhs.m2, ... k & rhs.mN

function <V> vecBitAnd(<V> lhs, <T> k)

Returns lhs.m1 & k, lhs.m2 & k, ... lhs.mN & k

function <V> vecBitXor(<V> lhs, <V> rhs)

Returns lhs.m1 ^ rhs.m1, lhs.m2 ^ rhs.m2, ... lhs.mN ^ rhs.mN

function <V> vecBitXor(<T> k, <V> rhs)

Returns k ^ rhs.m1, k ^ rhs.m2, ... k ^ rhs.mN

function <V> vecBitXor(<V> lhs, <T> k)

Returns lhs.m1 ^ k, lhs.m2 ^ k, ... lhs.mN ^ k

function <V> vecShl(<V> lhs, <V> rhs)

Returns lhs.m1 << rhs.m1, lhs.m2 << rhs.m2, ... lhs.mN << rhs.mN

function <V> vecShl(<T> k, <V> rhs)

Returns k << rhs.m1, k << rhs.m2, ... k << rhs.mN

function <V> vecShl(<V> lhs, <T> k)

Returns lhs.m1 << k, lhs.m2 << k, ... lhs.mN << k

function <V> vecShr(<V> lhs, <V> rhs)

Returns lhs.m1 >> rhs.m1, lhs.m2 >> rhs.m2, ... lhs.mN >> rhs.mN

function <V> vecShr(<T> k, <V> rhs)

Returns k >> rhs.m1, k >> rhs.m2, ... k >> rhs.mN

function <V> vecShr(<V> lhs, <T> k)

Returns lhs.m1 >> k, lhs.m2 >> k, ... lhs.mN >> k

Conversion Functions

function <Type>.appendDesc(io String string)

New in version 1.12.0.

The appendDesc method is called to convert the given type to a String. You can write a custom appendDesc method to customize this conversion, as shown in the following example:

/*
** Example: Custom appendDesc Method
*/

struct Vec3 { Float32 x, y, z; };

function Vec3(Float32 x, Float32 y, Float32 z) {
  this.x = x; this.y = y; this.z = z;
}

function Vec3.appendDesc(io String string) {
  string += "vec3:[";
  string += this.x;
  string += ":";
  string += this.y;
  string += ":";
  string += this.z;
  string += "]";
}

operator entry() {
  Vec3 vec3(6.7, -9.4, 2.3);
  report(vec3);
}

/*
** Output:

vec3:[+6.7:-9.4:+2.3]

*/
function String hex(UInt8 n)
function String hex(UInt16 n)
function String hex(UInt32 n)
function String hex(UInt64 n)

Converts an unsigned integer value into a hexadecimal string representation of the value.

function String hex(SInt8 n)
function String hex(SInt16 n)
function String hex(SInt32 n)
function String hex(SInt64 n)

Converts an integer value into a hexadecimal string representation of the value. The output is as if n was of the corresponding unsigned integer type; there is no consideration for negative values.

function Float32 bitcastUIntToFloat(UInt32 n)
function Float64 bitcastUIntToFloat(UInt64 n)

Bitcasts an unsigned integer of the same width to a floating-point number. This is a non-numerical conversion that is mostly useful for unit testing KL itself.

function UInt32 bitcastFloatToUInt(Float32 x)
function UInt64 bitcastFloatToUInt(Float64 x)

Bitcasts a floating-point number to an unsigned integer of the same width. This is a non-numerical conversion that is mostly useful for unit testing KL itself.

Performance Counter Functions

KL provides access to high-performance system timer information that can be used to time operations from within KL code.

function UInt64 getCurrentTicks()

Returns the current value of the performance counter. This number has no meaning on its own (ie. its units are undefined) but can be used in calls to getSecondsBetweenTicks() to measure absolute elapsed time. Note that the value returned by getCurrentTicks() is not affected by system (“wall”) clock time changes.

function Float64 getSecondsBetweenTicks(UInt64 start, UInt64 end)

Returns the number of seconds between two performance counter values. The measurable resolution is guaranteed to be at least one million parts per second.

Example usage of the performance counter functions:

operator entry() {
  UInt64 start = getCurrentTicks();
  // Do nothing...
  UInt64 end = getCurrentTicks();
  report("Elapsed time: " + getSecondsBetweenTicks(start, end) + " seconds");
}

Output:

Elapsed time: 4.1e-08 seconds

Memory Usage Functions

function UInt64 klHeapInUse()

Returns the number of bytes currently allocated on the KL heap. Memory is allocated on the KL heap in order to provide memory for variable arrays, dictionaries, objects, most strings, and some other less-commonly used types.

Note

This function cannot be called on the GPU

/*
** Example: klHeapInUse()
*/

operator entry() {
  report("klHeapInUse() before: " + klHeapInUse());
  Float32 a[](16); // allocates some memory on the KL heap
  report("klHeapInUse() after: " + klHeapInUse());
}

/*
** Output:

klHeapInUse() before: 0
klHeapInUse() after: 104

*/

Fabric Context Functions

These functions are used to interact with the Fabric Core context.

function String fabricCoreContextID()

Returns the Fabric Core context ID as a String. This context ID can be used to bind a new Fabric Core client to an existing context.

Named Constants

A named constant in KL is a value that can be referred to by name in expressions but that cannot be changed at runtime. Named constants are essentially read-only variables; however, since the KL compiler knows that their value can never change, it can often produce faster code when named constants are used in place of variables. Both scalar and array named constants can be declared.

Named constants can be declared within any scope (see Scoping Rules), including the global scope. Named constants are only visible within the scope in which they are declared.

Scalar named constants take the form:

const Type name = expr;

and array named constants take the form:

const Type name[] = [
  expr1, expr2, ..., exprN
];

In either case, Type must be a boolean, integer, floating-point or string type; name must be an identifier; and expr must be an expression involving constant(s) that evaluates to a constant of type {Type}. In the case of a scalar named constant, the type of the named constant is Type. In the case of an array named constant, the type of the named constant is a fixed array of elements of type Type; the size of the fixed array is the number of initializing values given within the brackets.

It is a compile-time error to do any of the following:

  • assign to a named constant
  • pass a named constant to a function as an io parameter
  • declare a global named constant with the same name as a function, operator or another global named constant
  • declare a non-global named constant with the same name as a variable or another named constant declared in the same scope

Example usage of named constants:

const String MODULE_NAME = "KL";
const String PREFIX = MODULE_NAME + ": ";
const UInt32 twoToTheSixteen = 1 << 16;
const Float32 familiarValues[] = [3.141, 2.718, (3 * 7.4) / 3.4];

operator entry() {
  report(PREFIX + "twoToTheSixteen = " + twoToTheSixteen);
  report(PREFIX + "familiarValues = " + familiarValues);
  for (UInt32 i=0; i<4; ++i) {
    const UInt32 a = 3;
    const UInt32 b = 4;
    report(PREFIX + "a*"+i+"+b = "+(a*i+b));
  }
}

Output:

KL: twoToTheSixteen = 65536
KL: familiarValues = [+3.141000,+2.717999,+6.529411]
KL: a*0+b = 4
KL: a*1+b = 7
KL: a*2+b = 10
KL: a*3+b = 13

Predefined Constants

There are a variety of predefined constants available to every KL program.

Fabric Version Pre-Defined Constants

The three constants FabricVersionMaj, FabricVersionMin and FabricVersionRev are three predefined constants of type UInt8 that are the major, minor and revision components of the running Fabric version. For example, this documentation was built for Fabric version 2.3.1, and so KL code executed in this version will have FabricVersionMaj = |FABRIC_VERSION_MAJ|, FabricVersionMin = |FABRIC_VERSION_MIN| and FabricVersionRev = |FABRIC_VERSION_REV|.

operator entry() {
  report("FabricVersionMaj = " + FabricVersionMaj);
  report("FabricVersionMin = " + FabricVersionMin);
  report("FabricVersionRev = " + FabricVersionRev);
}

Integer Limit Pre-Defined Constants

For every integer type <IntTy> there is a pre-defined integer constant <IntTy>Max that is the maximum value the integer can attain. Additionally, for signed integer types there is a pre-defined integer constant <IntTy>Min that is the minimum value the integer can attain. In both cases, the type of the integer constant is the type of the integer itself. For example:

operator entry() {
  report("UInt8Max=" + UInt8Max);
  report("UInt16Max=" + UInt16Max);
  report("UInt32Max=" + UInt32Max);
  report("UInt64Max=" + UInt64Max);
  report("SInt8Min=" + SInt8Min);
  report("SInt8Max=" + SInt8Max);
  report("SInt16Min=" + SInt16Min);
  report("SInt16Max=" + SInt16Max);
  report("SInt32Min=" + SInt32Min);
  report("SInt32Max=" + SInt32Max);
  report("SInt64Min=" + SInt64Min);
  report("SInt64Max=" + SInt64Max);
}

produces:

UInt8Max=255
UInt16Max=65535
UInt32Max=4294967295
UInt64Max=18446744073709551615
SInt8Min=-128
SInt8Max=127
SInt16Min=-32768
SInt16Max=32767
SInt32Min=-2147483648
SInt32Max=2147483647
SInt64Min=-9223372036854775808
SInt64Max=9223372036854775807

The FUNC Pre-Defined Constants

The KL compiler automatically predefines the constant FUNC at the start of every function as a string constant describing the function. The following code:

function foo(Float32 x) {
  report("This function is: " + FUNC);
}

operator entry() {
  foo(3.14);
}

produces the output:

This function is: function foo(Float32)

Importing Functionality With require

Through integration with Fabric, it is possible for derived KL types and/or Fabric extensions to provide KL code that is defined externally to the current source file. To use these types and code within the current source file, the require statement is provided; it is similar to the import statement in Python.

The require statement should be followed by the name of the registered type or extension. For example, to include the functionality provided by the extension named “Math” and the registered type named “RegType”, the program should start with:

require Math, RegType;

Any require statements must appear at the top of the KL program that uses the associated functionality. You can have as many require statements as you would like.

Using require with version information

By default the require statement will load the latest version of the extension available. So for example given two versions of the ExtensionName extension with the versions "1.0.0" and "1.2.1", doing

require ExtensionName;

will result in the version "1.2.1" being loaded. If you want to load a specific version, you can use the following syntax

require ExtensionName:"=1.0.0";

which will result in loading the specific "1.0.0" version of the extension. If the specific version cannot be found, an error will be thrown. Alternatively, if you just want to make sure an extension version is higher than a specific version number, you can use the lesser / greater sign like so:

require ExtensionName:">1.0.0";

If the version lesser / greater than what is specified cannot be found, an error will be thrown. In this example the "1.2.1" version of the extension will be loaded.

Furthermore you can use preprocessor statements to add optional KL code or to switch behaviors based on extension versions. For that you can use the EXT_VER_IF: and :code:`EXT_VER_ENDIF statements. For this you can use the equal, lesser or greater sign.

require ExtensionName;

EXT_VER_IF ExtensionName:">1.0.0"

function dummyFunction() {
  report('Found an extension version for "ExtensionName" higher than "1.0.0"');
}

EXT_VER_ENDIF

operator entry() {

EXT_VER_IF ExtensionName:">1.0.0"

  dummyFunction();

EXT_VER_ENDIF

}

The dummy function’s definition and invocation will only happen if the extension version of the ExtensionName is higher to "1.0.0" in the example above.

If you wish to check the version of Fabric itself, use Fabric as the extension name:

/*
** Example: Conditional Compilation using Preprocessor Statements
*/

EXT_VER_IF Fabric : "< 2.0.0"
foo() {
  report("Version is < 2.0.0");
}
EXT_VER_ENDIF

EXT_VER_IF Fabric : ">= 2.0.0"
foo() {
  report("Version is >= 2.0.0");
}
EXT_VER_ENDIF

operator entry() {
  foo();
}

/*
** Output:

Version is >= 2.0.0

*/

For more information on how to embed versioning information in extensions please refer to The version Extension Versioning Specification (optional).

Extension versioning environment variables

Additional to the facilities mentioned above in Using require with version information you can drive the require statement with a set of environment variables. There are several ways to use environment variables.

The first approach, which uses a single environment variable for each extension, defines an environment variable like so:

export FABRIC_EXT_VER_EXTENSIONNAME="=1.0.0"

For the second approach, which suits environments better when you have to switch between a large amount of environment variables for a given build set, first you may optionally specify the FABRIC_EXT_VER_PREFIX and FABRIC_EXT_VER_SUFFIX environment variables, which contain a prefix and a suffix to be used when looking up additional environment variables, or you can use their default values.

Then for each extension you may specify an environment variable using the prefix and suffix and the extension’s name, which will then contain the versioning information. For example, using the default prefix and suffix:

export FABRIC_EXT_VER_EXTENSIONNAME="=1.0.0"

Or changing the prefix and suffix:

export FABRIC_EXT_VER_PREFIX="COMPANY_"
export FABRIC_EXT_VER_SUFFIX="_VER_INFO"
export COMPANY_EXTENSIONNAME_VER_INFO="=1.0.0"

This would resolve in the extension loading mechanism to first resolve the FABRIC_EXT_VER_PREFIX and FABRIC_EXT_VER_SUFFIX environment variables, then will resolve the COMPANY_EXTENSIONNAME_VER_INFO based on these and will figure out that version 1.0.0 of the two available versions should be used.

The third approach uses an auxiliary json file which needs to provide a mapping between the name of the extension and a version to use. The file path of the json file needs to be specified in the FABRIC_EXT_VERFILE environment variable. The content of the file needs to look for example like this:

{
  "Alembic": ">=1.0",
  "FBX": ">=1.1"
}

In the final approach users may set the FABRIC_EXT_OVERRIDE environment variable which can be used to specify sets of extensions that should be loaded together. Each extension may define an override key (see The version Extension Versioning Specification (optional)) and if that key matches the value of FABRIC_EXT_OVERRIDE then that extension will be loaded before others with a different or missing override key. This may cause lower version numbers of extensions to load. For example:

export FABRIC_EXT_OVERRIDE="MyOverride"

This will cause all extensions with an override key of “MyOverride” to be preferred over other versions of the same extension with a different override key.

Note

All environment variables need to use capital letters throughout.

For more information on how to embed versioning information in extensions please refer to The version Extension Versioning Specification (optional).