cowlark.com :  cowbel :  The language

cowbel

The language

Published: 2016 November 3

Introduction

Cowbel is a block-structured language with a syntax deliberately modelled on Javascript. There should be few surprises. (But see the operator precedence table in the section on expressions below.)

Unlike Javascript, semicolons are mandatory, but only in certain statements. (Statements that terminate with a substatement, such as while, do not require a terminating semicolon.)

Preprocessing

Cowbel files are run through the C preprocessor. As such, standard comments using /* ... */ and // will work, as do include files with #include.

C macros may also be used, but are not recommended.

Statements

The following control-flow statements are supported:

if CONDITION POSITIVE-STATEMENT
if CONDITION POSITIVE-STATEMENT else NEGATIVE-STATEMENT
while CONDITION LOOP-BODY-STATEMENT
do LOOP-BODY-STATEMENT while CONDITION;
for VARIABLE = FROM-EXPRESSION, TO-EXPRESSION [, STEP-EXPRESSION] LOOP-BODY-STATEMENT
continue
break

continue and break work in the obvious manner.

Note well! The range in for is treated as [from, to). That is, the target number is exclusive. And the loop uses equality to compare against this target number. This means that the following code will loop forever:

for i = 0, 10, 8
{
  print("i will never be equal to 8");
}

Arbitrary jumps are supported using goto and labels:

LABEL:
goto LABEL

Jumps are allowed only within the current scope or to an enclosing scope, and then only within the current function.

Variables can be declared anywhere.

var VARIABLE-NAME: TYPE = INITIALISER-EXPRESSION
var VARIABLE-NAME = INITIALISER-EXPRESSION

The type declaration is optional. If not type is specified, the variable's type is inferred from the initialiser. The initialiser is mandatory.

Multiple variables may be declared, and then initialised from an initialiser list.

var VAR1: TYPE1, VAR2: TYPE2, VAR3: TYPE3 = INIT1, INIT2, INIT3
var VAR1, VAR2, VAR3 = INIT1, INIT2, INIT3

Note that this is not how Javascript handles things.

Assignments in cowbel are considered to be statements, not expressions.

VAR = VALUE-EXPRESSION
VAR1, VAR2, VAR3 = VALUE1, VALUE2, VALUE3

When assigning to multiple values, the values are all evaluated before assignment, to allow this construct:

a, b = b, a

Multiple values may be returned from functions and methods:

VAR1, VAR2, VAR3 = FUNCTION-NAME(ARGUMENTS...)
VAR1, VAR2, VAR3 = OBJECT-EXPRESSION.METHOD-NAME(ARGUMENTS...)

The number of output variables must be equal to or fewer than the number of values returned by the function or method. Extra return values are discarded.

In addition, there are statements for declaring functions and types, which are described below; and a bare expression may also be used as a statement.

Functions

Functions may be declared as follows:

function FUNCTION-NAME(INPUT-ARGUMENTS): (OUTPUT-ARGUMENTS)
   FUNCTION-BODY

For example:

function f1(i1: int, i2: int): (o1: int, o2: int) {}
function f2(): () {}

Values are returned by assignment to a named output value.

function f(): (o1: int, o2: int)
{
  o1 = 1;
  o2 = 2;
}

The return statement can be used to exit a function early.

function f(): (o1: int)
{
  o1 = 1;
  return;
  o1 = 2; /* not reached */
}

As a special case, return can also be used to specify a value for a function that has exactly one output value.

function f(): (o1: int)
{
  return 1;
}

As a special case, functions that return no values may omit the output argument list. These are equivalent:

function f1(i1: int): () {}
function f2(i1: int) {}

As a special case, functions that return one value may use an abbreviated output argument list. These are equivalent:

function f1(i1: int): (i2: int) {}
function f1(i1: int): int {}

In this situation, the output argument becomes anonymous, and can only be assigned to with the second form of the return statement, described above.

Expressions

Expressions consist of a set of nested function or method calls, grouped by parentheses. The set of precedence is as follows:

lowest   infix operators
         prefix operators
         method calls or function calls
highest  parentheses, constants, identifiers

Note that this means that all infix operators have the same precedence, which means that his expression:

1 + 2 * 3

...is evaluated strictly left to right and therefore may not produce the result you expect.

Operators become ordinary method calls. Infix operators call the named method with no arguments; prefix operators with one argument.

Traditional method calls have the following syntax:

OBJECT-EXPRESSION.METHODNAME(ARGUMENTS...)

There is (currently) no short-circuiting inside expressions.

Types

cowbel supports the following primitive types.

boolean    true/false boolean
int        32-bit integer
real       64-bit floating point
string     immutable UTF-8 string

It is possible to assign type aliases with the type statement.

type NEWNAME = OLDNAME;

In a type context, a {...} block defines a new interface type.

type NAME = { INTERFACE-STATEMENTS... };

Currently the only interface statement supported is a method declaration, which is the same as a function declaration with no body. For example:

type HasString =
{
  function toString(): string;
};

Note the trailing semicolon above!

The only implicit type conversion supported is from an interface to a subinterface.

Operator methods are defined in the obvious way:

type HasAddition =
{
  function + (other: HasAddition): HasAddition;
};

Constants

Integer constants can currently be only in decimal.

Real constants must contain a decimal point (to distinguish them from integer constants).

Boolean constants must be either true or false.

String constants must be delimited with either '...' or "...". The following escape characters are supported:

  • \n = newline (ASCII 10)
  • \r = carriage return (ASCII 13)
  • \", \', \\ = ", ' or \, respectively

Objects

Cowbel considers scopes and objects to be the same thing: a block scope is simply an object constructor which discards its result, while an object constructor is a block scope where the result is recorded. Functions in the scope become methods and variables in the scope become instance data. Other statements in the scope are executed in the usual fashion when the object is created.

For example:

var object =
{
  function greet()
  {
    print("Hello, world!");
  }
};

object.greet();

An object constructor returns a value which has the type of an anonymous interface implementing all the methods in the scope. This type has no name and cannot be referred to.

The object can be declared to implement one or more named interfaces using the implements keyword.

implements TYPE;

For example:

var object =
{
  implements HasString;

  function toString(): string
  {
    return "object";
  }
};

var asinterface: HasString = object;

It is possible to use the implements keyword in a block scope, but not useful.

Scoping

Variable, function and type declarations in cowbel are hoisted to the top of the scope in which they are defined. This means that no special action is needed to forward declare functions.

That said, referring to variables before they have been initialised can be problematic.

print(i); /* i's value is invalid */
var i = 1;

[Note: the above code should be rejected by the compiler; but currently there is no dataflow analysis, so it's accepted. Please don't do this.]

Variables may be referred to and modified in nested functions and objects in their scope. True upvalues are supported.