NewLang v0.1.0

Featured image

Attention!!!

This article contains a description of the syntax of the previous version of NewLang.

The current version of the language syntax can be viewed here.

NewLang is a high-level programming language in which one can combine standard algorithmic constructs with declarative programming and tensor computations for machine learning tasks.

The main feature of the language is a simple, logical and non-contradictory syntax, which is based not on the use of reserved keywords, but on a strict system of grammar rules using punctuation marks (the list of which also includes the operators of the language).

What is new compared to the previous release:

  • All the main algorithmic constructs have been implemented: condition checks, various kinds of loops, pattern matching, interrupting a sequence of commands and returning data, error handling, etc.
  • The base syntax for individual language constructs has been reworked.
  • Most operations with variables have been implemented, including the list and tensor expansion operators.
  • The system of built-in types has been seriously reworked, functions for their conversion and for defining data (the latter can be called data comprehensions) have been implemented.
  • New tests have been added, etc.

Why is NewLang needed?

All modern programming languages undergo constant development (read: complication) of the syntax as new versions are released. This is a kind of payment for the appearance of new capabilities and is perceived by users as a natural phenomenon.

But at the same time it is also a serious problem, because with the release of new versions new keywords and syntactic constructs are added, which inevitably raises the entry threshold for new users. Another consequence of this process is the constant increase in the complexity of development and the labor intensity of maintaining already created software products, when old code is refined using already-new standards.

In NewLang the complexity of language constructs is naturally limited through the division of the language syntax into two parts, which simplifies its study and use.

The base syntax — for writing programs in the object-oriented (imperative) and declarative styles, which is based not on reserved keywords, but on strict grammar rules, and the extended syntax — when the base syntax becomes insufficient, or an implementation-language construct needs to be used.

Another inconvenience of modern mainstream languages is that most of them were created before the beginning of the era of machine learning, therefore tensor computations in them are implemented as separate libraries, and not built into the base syntax of the language and the system of base types.

In NewLang, tensor computations are available “out of the box” (the libtorch library is used), and ordinary numbers are scalars (tensors of zero dimension).

NewLang syntax

When developing the syntax, I tried to adhere to the rules already established in the IT industry, so as not to generate multiple meanings that depend on the context.

Basics

  • Statements are separated by a semicolon “;”.
  • Indentation and line breaks are ignored (I very much wanted to have the possibility of automatic code formatting).
  • Multiline comments in the source code correspond to the C/C++ style and must be placed between the characters /* and */. Nesting of multiline comments is not supported.
  • Single-line comments start with the “#” character up to the line break, which corresponds to comments in the Python and Bash style.
  • A sequence of commands that must be executed as a whole is enclosed in curly braces “{}”.
  • Extended-syntax program inserts in the implementation language are enclosed in curly braces with a percent sign %{ /* any C/C++ code can be here */ %}, as in the lex and flex lexers.

Creating objects and assigning new values

NewLang uses three different operators to create objects and assign new values to them:

  • ::= is used only to create new objects, and if an object with such a name already exists, an error is generated.
  • := is used for the same purposes, but if an object with such a name already exists, no error occurs, and the new value is assigned to the already existing object.
  • = is applied only to assign a value to already existing objects, and if an object with the specified name is absent, an execution error also occurs.

Using three different operators for creating/changing objects makes it possible to control such operations more flexibly and to detect logical errors in the code at an earlier stage.

var ::= 1.0; # Create a new variable var without specifying a type
var = 100; # Assign a new value to an already existing variable
printf := @import('printf(format:Format, ...):Int'); /* Create a new or redefine the object printf, which will be the result of executing the global function @import */

Object identifiers and modifiers

As identifiers one can use letters, digits and underscores in any combinations, provided that the first character of the identifier is not a digit.

NewLang has the possibility of specifying the scope and lifetime of an object using a modifier — a special character before the variable name. This may seem a bit similar to Hungarian notation, but unlike it, the modifier has nothing to do with the type of the object and is not part of the identifier name. Moreover, strictly defined characters are used as modifiers, whose purpose is determined in advance.

  • $ — at the beginning of a name denotes a local variable whose lifetime is limited by the current scope, and when it ends the local variable is destroyed.
  • @ — denotes a global variable, and the object itself retains its state even after leaving the current scope.
  • : — the colon at the beginning of a name is used as a modifier to indicate the type.

The semantics of accessing function arguments is very similar to working with arguments in bash scripts, where $1 or $arg is the ordinal number or the name of the argument (access occurs to local variables in the current scope).

The use of modifiers is mandatory only in two cases:

  • When creating a new data type, since types are always created in the global scope, and their symbolic names must be unique.
  • When accessing NewLang objects inside program inserts of code in the implementation language, since they are used as markers when searching for NewLang identifiers in C/C++ code.

In the remaining cases, it is not mandatory to specify their modifiers to access variables. And if no modifier is specified when accessing an object, then first a local variable is searched, and then a global one with the same name. Moreover, the local variable will shadow the global one.

One should also keep in mind that the compiler can generate code for direct access to local objects already at compile time, whereas for accessing global objects, or if the scope modifier is absent, the compiler is forced to embed a runtime call of the object search function in the global symbol table every time.

Type system

Since the type system of the language is dynamic, an explicit type indication does not affect the size of a variable and is only a kind of logical restriction on the possibility of assigning a value of another type to the variable.

Type information is used when checking their compatibility, when a value of another type is assigned to an existing object. Such an operation is possible only when the types are compatible with each other and allow automatic casting. This is true both during the parsing/compilation of the source text and during execution in interpreter and/or compiled file modes.

Simple types

Arithmetic values

Arithmetic data types are tensors — arrays of numbers of one type with an arbitrary number of dimensions and the same column size in each. A single number is also a tensor of zero size.

Only signed integers are supported, since there is no particular need for unsigned numbers, and many problems with them can be found out of the blue.

Problems of unsigned numbers (from the internet):

First, the subtraction of two unsigned numbers, for example 3 and 5. 3 minus 5 equals 4294967294, since -2 cannot be represented as an unsigned number. Second, unexpected behavior can occur when mixing signed and unsigned integer values. C++ can freely convert signed and unsigned numbers, but does not check the range to make sure you are not overflowing your data type. In C++ there are still a few cases where one can (or must) use unsigned numbers. First, unsigned numbers are preferable when working with bits. Second, the use of unsigned numbers related to array indexing.

But this is not my case, since an index can be negative and even not a number, but a range or an ellipsis.

P.S. And even knowing this, I still recently managed to catch a bug with negative indices of dictionaries!

The names of the built-in arithmetic types speak for themselves: Char, Short, Int, Long, Float, Double, ComplexFloat, ComplexDouble. The boolean type Bool is a separate type, which can take only the values 0 or 1 (false/true respectively), and depending on the operation performed can be attributed to integer types or not be part of them (this approach to interpreting the boolean data type was taken from the Torch library).

// Treat bool as a distinct "category," to be consistent with type promotion
// rules (e.g. `bool_tensor + 5 -> int64_tensor`). If `5` was in the same
// category as `bool_tensor`, we would not promote. Differing categories
// implies `bool_tensor += 5` is disallowed.
//
// NB: numpy distinguishes "unsigned" as a category to get the desired
// `bool_tensor + 5 -> int64_tensor` behavior. We don't, because:
// * We don't want the performance hit of checking the runtime sign of Scalars.
// * `uint8_tensor + 5 -> int64_tensor` would be undesirable.

In the future it is planned to add number classes for long arithmetic and fractions, for which the type names BigNum, Currency and Fraction are reserved.

Access to the elements of a tensor occurs by an integer index that starts at 0. For a multidimensional tensor, the indices of an element are listed in square brackets separated by commas. Access to elements through a negative index is supported, which is handled exactly as in Python (-1 the last element, -2 the penultimate one, etc.).

A tensor literal in the program text is written in square brackets with a mandatory trailing comma, i.e. [1, 2,] is a literal one-dimensional tensor of two numbers. After the closing bracket, the type of the tensor can be specified explicitly. If the type is not specified, then it is inferred automatically on the basis of the specified data and the minimally possible byte size is chosen that allows storing all values without loss of precision.

Examples:

$var_char := 123; # The type Char is inferred automatically
$var_short := 1000; # The type Short is inferred automatically
$var_bool := [0, 1, 0, 1,]; # A tensor of 4 elements. The type Bool is inferred automatically
$tensor[10,10]:Int := 1; # A tensor Int of size 2x2 initialized with 1
$scalar := $tensor[5,5]; # Assign to a scalar the value of the specified tensor element

Strings

Two types of strings are supported: StrWide — character (wide) and StrChar — byte. The difference between them lies in the type of the single element. For character strings the single element is the wide character wchar_t, and for a byte string the single element is one byte (more precisely char, i.e. a signed byte). Character string literals in the source text are written in “double quotes”, and byte strings in ‘single quotes’.

The number of elements of a character string is returned in wide characters, and the size of a byte string in bytes, therefore access to a string element by index occurs respectively either to a character or to a byte.

An important point. Any variable can be addressed in the same way as a function (by writing parentheses after its name). The result of this operation will be the creation of a copy/clone of the object. Moreover, some types (dictionaries, classes and character strings) can be used as a template when creating a copy of an object with modified properties, if the new and/or changed values are specified in parentheses, as arguments in function calls. So, if when creating a copy one specifies a set of new data in parentheses, then the resulting copy will contain the already changed data.

For example:

$template := "${name} $1"; # An ordinary string
$result := $template("template", name = "String"); # result = "String template"

System types

:Pointer — a pointer to a system memory area

Since any program has to interact with the outside world, one is forced to provide for the possibility of using other libraries and type systems, and for these purposes the type Pointer serves. It is created when importing functions from external libraries and cannot be created manually. But one can output its value, for example for debugging.

:Plain — a pointer to the binary representation of data

Interaction with external libraries also requires data exchange. And for these purposes the data type Plain serves — which is also a pointer, but to the binary representation of data in a single memory area. Of course, if they can be represented as a single fragment.

Composite data types:

Dictionary

A dictionary (:Dictionary) is a set of data of an arbitrary type with access to individual elements by an integer index or by the name of the element (if present) (it resembles both a tuple and a structure at the same time). Dictionaries differ from tensors in that they are only one-dimensional arrays, but each element can contain an arbitrary number of elements of any type, including other dictionaries.

Access to the elements of dictionaries occurs by the name of the element, which is written through a dot after the name of the variable, or by an integer index. The index also starts at 0 and, like tensors, can also be negative.

A literal of the “dictionary” type in the program text is written in parentheses with a mandatory trailing comma, i.e. (,) is an empty dictionary, (1, 2= “2”, name=3,).

Enumeration, structure and union

:Enum, :Struct and :Union are the same dictionaries, only certain restrictions are imposed on their elements. Each element must have a unique name, and its data type must be simple, i.e. a number or a string of fixed size.

Classes

A class (implemented partially) is a data type with the help of which one of the principles of OOP is implemented — inheritance. When creating an instance of a class, a new variable is created that retains information about its parent and inherits properties and methods from it. The data type :Class is similar to a dictionary, but all properties must have names (although access to the properties of a class by index is also possible).

Functions

The NewLang syntax supports several types of functions (and in the future also class methods): ordinary functions, pure functions and simple pure functions.

Default arguments are supported for all types of functions. When creating a function, its arguments are specified as in Python, i.e. first come the mandatory arguments, then the arguments with default values, where the name of the argument is separated from its default value by the equals sign =. If a function allows handling an arbitrary number of arguments, then an ellipsis … (three dots in a row) is specified last in the parameter list.

Ordinary function

An ordinary function — such functions are precisely ordinary functions in the C/C++ understanding. Inside them one can write absolutely any code, including condition checks, loops, calls of other functions, etc.

Inside an ordinary function one can access local and global objects, and they can contain inserts in the implementation language*, for example to call functions from external libraries.

Inserts in the implementation language are formatted as %{ %} and can contain any C/C++ text, and directly from it one can access local and global NewLang objects in the same way as in ordinary syntax, by specifying the corresponding modifier as the first character of the name ($ for local objects and @ for global ones).

Technically, such a program insert is simply transferred by the transpiler directly into the source text of the generated file, and all NewLang identifiers are decorated in a special way (special markers are added for their identification), after which the source text is fed to the input of an ordinary C++ compiler. For local objects the transpiler can generate code for direct access to the object at compile time, while for working with global objects it is forced to use runtime calls of the search function in the symbol table.


*) — Program inserts in the implementation language are processed only during compilation

For example:

print(str) := { 
    %{ 
        printf("%s", static_cast<const char *>($str)); /* A direct call of a C function */ 
    %} 
};

Pure functions

A pure function is also an ordinary function, only in the sense that functional programming puts into it. The creation of a pure function occurs with the help of the operator :-, and the operator itself is borrowed from the Prolog language. A pure function has no access to the context and global variables, therefore it can process only the data that were passed to it as arguments.

Program inserts in the implementation language inside pure functions are not forbidden and can be used, for example, for debugging. But this is done at the developer’s own risk. It is he who is responsible for their “purity”, for example when calling functions from external libraries.

Sum(arg1, arg2) :- {$arg1+$arg2;}; # Return the sum of the arguments

Simple pure functions

Simple pure functions are a separate class of pure functions that are intended only for computing a logical result (i.e. they are predicates) and are distinguished by a simplified notation form. The body of a simple pure function consists of a sequence of statements separated by commas and ending, like any expression, with a semicolon. All the statements of a simple pure function are always reduced to a boolean value, and the final result of the function is computed by one of the possible logical operations: AND, OR and exclusive OR.

For example:

func_and(arg1, arg2) :&&= arg1==3, arg2 > 0;  # A simple pure function, logical AND
func_or(arg1, arg2) :||= arg1==3, arg2 > 0; # A simple pure function, logical OR
func_xor(arg1, arg2) :^^= arg1==3, arg2 > 0;  # A simple pure function, exclusive OR

Special data types:

Empty (:None)

:None (empty) — contains no value (more precisely it has a single value None) and is compatible with any other data type. It is indicated in the program text as a single underscore “_”. Uninitialized variables have None, and an attempt to read from such a variable raises an error.

The type of a variable can be specified explicitly or inferred automatically from the assigned value. A new value can be assigned to an already initialized variable only for a compatible type, since implicit type conversions are not allowed.

$var := _; # Create an uninitialized variable
$var2 := var; # Error!!! Cannot read the uninitialized variable var
$var = 1000; # The variable will have the type Short (the minimum size for storing the value)
$var = 0,5; # Error!!! Short ← Float are incompatible
$var = _; # Clear the value of the variable
$var = 0,5; # Now it is possible, since None is compatible with any type

Range (:Range)

A range is a special data type that is an approximate analog of the “generator” type in Python. A range can be addressed as an iterator and it will alternately yield elements in the specified interval with a given step. A range in the program text is specified as two or three elements separated by two dots, for example 1..5 — a range from one to five with the default step 1. As the parameters of a range one can specify not only literals, but also variable names. For example, 0,1..$stop..0,1 — a range from 0.1 to the value specified in the variable $stop with step 0.1.

A range for integers can be used as an index of tensors (more precisely, of any objects that allow access to their elements by index, i.e. tensors, dictionaries and text strings). In fact, this behavior is analogous to slice in the Python language, and array[1:5] in Python means the same as array[1..5] in NewLang.

As an index of tensors one can also specify an arbitrary number of dimensions with the help of an ellipsis, i.e.

$tensor[…, 0] = 0; # Zero out all the first elements in each dimension.

Type conversion

Explicit type casting

Despite the dynamic typing of the language, if the type of a variable is specified explicitly, then automatic type casting is not performed, and to assign a value of an incompatible type to the variable an explicit conversion is required.

Since the symbolic names of types relate to implementation details, an explicit conversion to a concrete data type is performed by calling a function with the name of the type, i.e. :Bool(), :StrWide(), :Long(), etc.

To convert any data type to a string, one can also use the string concatenation operator, which converts any data type to a string representation. But, since there are two string types (byte and wide strings), the string type is determined by the first argument in the concatenation/joining operator ++. Also, any value can be converted to a string with the help of a template string.

"" ++ 123;   # "123" - a wide character string
'' ++ 123;   # '123' - a byte string

val := 12345;   # A number
"{}"(val);      # Will be the string "12345"

Since tensors can have more than one value, they can also accept an arbitrary number of them as arguments, and the final result will be a tensor in which all the passed data are converted to the required type automatically.

Examples:

> tstr := :Tensor("Тест");   # Create a tensor from a wide character string
[1058, 1077, 1089, 1090,]:Int

> t2 := :Tensor[2,2]("Тест"); # The same, but a tensor of the specified dimensionality
[
  [1058, 1077,], [1089, 1090,],
]:Int

> :StrWide(tstr) # Create a character string from a tensor
Тест

> :Double(t2)    # Change the data type of the tensor without changing the dimensionality
[
  [1058, 1077,], [1089, 1090,],
]:Double

> t3 := :Char[4]( t2 ) # Change the dimensionality of the tensor and its type (in this case with partial data loss)
[34, 53, 65, 66,]:Char


>:Tensor( (1,2,3,) );  # A tensor from a dictionary
[1, 2, 3,]:Char

>:Tensor( 'first second' )  # A byte string into a tensor
[102, 105, 114, 115, 116, 32, 115, 101, 99, 111, 110, 100,]:Char

>:Tensor( (first='first', space=32, second='second',) )  # We get a tensor from a dictionary with the same data
[102, 105, 114, 115, 116, 32, 115, 101, 99, 111, 110, 100,]:Char

>:Double[10,2]( 0, ...)   # A tensor of the given format with zeros, where the ellipsis repeats the last specified data until a tensor of the required size is obtained
[
  [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,], [0, 0,],
]:Double

>:Int[3,2]( ... rand() ...)  # A tensor with random data, where between the ellipses the function is specified that should be called each time a new tensor element is obtained
# I had to invent a new construct, since an ellipsis before a name is the dictionary expansion operator, and an ellipsis after a name is the repetition of the last value until the end of the given dimensionality.
[
  [1804289383, 846930886,], [1681692777, 1714636915,], [1957747793, 424238335,],
]:Int

>:Int[5,2]( 0..10 );  # Creating a tensor from a range
[
  [0, 1,], [2, 3,], [4, 5,], [6, 7,], [8, 9,],
]:Int

>:Tensor( 0..0.99..0.1 );  # Or even like this
[0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9,]:Double

Operators and control constructs

Operators:

All operators have a paired analog with value assignment:

  • + and += — addition of arithmetic data types;
  • - and -= — subtraction of arithmetic data types;
  • / and /= — division (the result is a floating-point number);
  • // and //= — integer division rounded toward the smaller number (as in Python);
  • * and *= — multiplication;
  • ** and **= — exponentiation (it is also used to repeat text strings);
  • ++ and ++= — string concatenation with automatic casting of the arguments to the string type (the increment character is deliberately used instead of a single plus in order to explicitly separate string concatenation and arithmetic addition operators).

Comparison operators:

  • <, >, <=, >= — the classical ones for comparing scalars
  • == and != — comparison operators with automatic casting of compatible types for any objects
  • === and !== — the exact comparison operator for any objects (no automatic type casting is performed)

Type checks (of an object’s class name):

For the operator that checks the class name of an object, the tilde symbol ~ is used. It is somewhat similar to the instanceof operator in Java. The left operand must be the object being checked, and the right one the type being checked, which can be specified by a string literal, a string-type variable or the name of the class being checked directly. The result of the operation will be true if the right operand contains the name of the class of the object being checked or it is present in the inheritance hierarchy.

name := "class";    # A string variable with the class name
var ~ name;         
var ~ :class;       # A type name
var ~ "class";      # A string literal with the type name

Duck typing

The duck typing operator, two tilde symbols ~~, is an approximate analog of the isinstance() function in Python, which for simple types compares the immediate compatibility of the type of the left operand with respect to the right one. And for dictionaries and classes, the left operand is checked for the presence of all the field names present in the right operand, i.e.:

(field1="value", field2=2,) ~~ (); # True (i.e. the left operand is a dictionary)
(field1="value", field2=2,) ~~ (field1=_); # Also true (since the field field1 is present in the left operand)
(field1="value", field2=2,) ~~ (not_found=_); # False, since the field not_found is absent in the left operand

Strict duck typing ~~~ — for simple types the identity of types is compared without taking compatibility into account, and for composite types all properties are compared with the help of the strict equality operator. For this operation, the empty type is compatible only with another empty type.

Control constructs

The control constructs of the NewLang language include the conditional operator, two kinds of loops, the expression evaluation operator, the operator for interrupting a sequence of commands and interrupt capture. Condition check operators are always specified in square brackets, and the sequence of commands to execute in curly braces.

Conditional operator

As the condition check operator, a syntactic construct is used that in meaning corresponds to the term “follows”, i.e. a dash and an angle bracket -> or with two dashes for greater clarity –>. Such a notation of the conditional operator is very similar to a mathematical one and is easily combined into sequences for checking multiple conditions of the “else if” kind.

In the general case, the conditional operator has the form: [ condition ] -> action; or [ condition ] -> {action}; or [ condition1 || condition2 ] --> {action}, [_] --> {otherwise action};

For clarity, written with indentation:

[ condition1 ] -> { action1 },
    [ condition2 ] -> action2,
    [ condition3 ] -> action3,
    [_] -> {otherwise_action};

Expression evaluation

The syntactic construct with the help of which an analog of the switch operator is implemented looks as follows:

[ $var ] ==> {
    [1] -> { code };     # The condition $var == 1 will be checked
    [1, 2] -> { code };  # The condition ($var == 1 || $var == 2) will be checked
    [_] -> { code default };  # The otherwise branch
};

Moreover, as the operator for evaluation any of the available equality comparison operators can be used:

  • ==> — an equality check with type casting;
  • ===> — an exact equality check;
  • ~> — a type (class name) check;
  • ~~> — duck typing;
  • ~~~> — strict duck typing.

And if the duck typing operator is used as the comparison operator, then the expression evaluation turns into classical Pattern Matching:

$value := (f1=1, f2="2",);
[ $value ] ~~~> {
    [ (f1=_, ), (f1=_, f2=0, ) ] -> { code };     # The field f2 is absent or a number 
    [(f1=_, f2="",), (f1=_, f2='',)] -> { code }; # The field f2 is a string
    [_] -> { code default };                      # The default code
};

Loop operators

To specify loop operators, the control operators -» or –» are used (with two arrows in the direction from the condition check to the loop body). And depending on the arrangement of the condition and the loop body, it can be a precondition loop (while) or a postcondition loop (do while). Although these syntactic constructs have not yet “settled down” and can be considered temporary because they are easy to confuse with the conditional operator, and perhaps it makes sense to replace the different loop operators with a single construct of the form: <–> or «-», which is more strongly distinguished from the follow operator.

But at the present moment the loops are implemented like this:

[while condition] ->> {
  while loop body
};

{
  do while loop body
} <<-- [do while condition];

Implementation of the foreach loop using the example of summing all the elements of a dictionary (or a one-dimensional tensor):

summa := 0;
dict := (1,2,3,4,5,);
[ dict ] -->> {           # The loop condition, while there is data
  item, dict := ... dict; # The result of the dictionary expansion operator - its first element is moved to item
  summa += item;          # Computing the sum of all the elements of the dictionary
};

Interrupt (return) operators

Two minus symbols – are used as the interrupt/return operator. The operator makes it possible to interrupt the execution of a sequence of commands and/or return data from the current function/code block and is the closest analog of the return and throw operators simultaneously. In order to return data, they must be specified between two interrupt operators, i.e. --100--; # Return the specified value. If the return value is not specified explicitly, then the value None will be returned.

Sequence (code block/lambda function)

An algorithmic construct that is responsible for the sequential execution of several commands/statements and returns the result of the execution of the last of them. Also, the result of the execution can be a value that is returned with the help of the interrupt (return) operator. This is very similar to a classical lambda function, only it is executed immediately during its definition, and the result of its execution is saved into the variable.

A sequence without interrupt capture is formatted as a sequence of ordinary and curly braces, i.e. (){ run code }; or the same, but saving the result of the execution in a variable: $result := (){ run(); code() };. But if an interrupt operator is executed inside such a function, then it will never return control and will not save the returned value into the variable $result!

To capture interrupts, including returned values, one must use the sequence-with-interrupt-capture construct, which is formatted as a sequence of ordinary and double curly braces, i.e. $error := (){{ run(); error();code() }};. Such a construct captures all returns and interrupts if they occur during the execution of the sequence of commands. In this case any result will be saved in the variable $error both on normal completion and in the case of an error.

For finer tuning of interrupt capture, one should use the typed construct, when it is explicitly specified which types of interrupts should be captured. $runtime := (){{ run(); error(); code() }}:ErrorRuntime;. Such a construct will return a result only in the case of successful completion (when a non-typed value is returned with the help of the interrupt operator, for example, --"String"--;), or when an interrupt is created with the specification of a concrete type –:ErrorRuntime(“Error description”)–;. But if any other typed interrupt occurs, the value will not be captured and everything will work as in the very first variant, i.e. without capturing the interrupt and without saving the returned value into the variable.

Error handling strategy

Error handling consists of a combination of two elements: an operator for interrupting execution with the specification of the type of the returned value and an algorithmic construct “sequence” with the possibility of capturing an interrupt of a given type.

This differs a little from the classical variant of exception handling, which in ordinary programming languages is usually formatted with the keywords try… catch… finally with various variations. After all, the main goal of such syntactic constructs is to single out a section of code where an error may occur, and to catch and handle the correct type of data (exceptions), since NewLang does not distinguish between return operators and exception generation.

The approach to exception handling is as follows:

The program code that may lead to an error is enclosed in double curly braces, and the result of the execution of such a code block is assigned to a variable. After that the returned value is analyzed, for example by the pattern comparison operator:

$result := (){{     # the beginning of the analog of a try block
    $value := call_or_exception1();
    [condition1] -> { -- :Error -- };
    [condition2] -> { -- $value -- };
    $value := call_or_exception2();   
}};                 # the end of the analog of a try block
              
[$result] ~> {      # To compare by pattern, use the type check operator (class name)
    [:ErrorParser] -> {Parser error handling code};
    [:ErrorRunTime] -> {Runtime error handling code};
    [:Error] -> { Code for handling the remaining errors };
    [_] -> { Handling of normal data $value without errors };
};

Plans for the future

The current version of the language has been significantly enriched with capabilities, but is still a test platform for verifying the declared concepts and the base syntax. If we talk about plans, then at the present moment the roadmap of NewLang development is as follows:

  • Implement macros for a more familiar use of the language (through the use of DSL);
  • Add long numbers and fractions to the arithmetic types;
  • Make some kind of logical game (tic-tac-toe, sudoku or something similar) with an algorithmic choice of the next move and its computation using machine learning;
  • Write even more different examples to evaluate the syntax;
  • Fix the syntax taking into account the experience gained and the feedback;
  • Restore the operability of the compiler for generating executable files;
  • Do another big code cleanup;
  • Rework and document the resulting semantics of the language taking into account all the capabilities and release the first full-featured version of NewLang.