NewLang v0.3.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 general-purpose programming language. The main feature of the language is a simple, logical and non-contradictory syntax, which is based on a strict system of grammar rules. Through the use of macros, the syntax of the language is easily extended to a more familiar keyword-based form.

NewLang is in the process of active development. This is the third public release, in which new substantial capabilities have been added compared to the previous version.

Why is NewLang needed?

All modern programming languages undergo constant development (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, since with the release of 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 developing and maintaining already created software products, when old code is refined using already-new standards.

In NewLang the complexity of language constructs is limited naturally through the division of the language syntax into two parts — this 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 a system of strict grammar rules. There is a possibility of extending the base syntax through the use of macros. The extended syntax — program inserts in the implementation language (C/C++), when the base syntax becomes insufficient.

Another inconvenience of modern 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. The same applies to computations with unlimited precision, which also require the use of additional library functions.

In NewLang, tensor computations and rational numbers of unlimited precision are available “out of the box”. They are supported at the syntax level for writing literals of the corresponding types, while simple arithmetic data types are scalars (tensors of zero dimension). The implementation of tensor computations is made on the basis of the libtorch library, and rational numbers using OpenSSL.

New global features and changes in the syntax:

Simple pure functions removed

Earlier I tried to use the predicate syntax from Prolog, in which condition checks are written with commas. But this idea turned out to be a failure, and the operators themselves simply terrible (I tested different variants, but each time I got a lot of negative feedback), so now I decided to abandon them completely.

They turned out not only difficult to perceive, but also in fact unnecessary, since they were easily replaced by an ordinary function with a combination of ordinary logical operations.

The syntax of the operators for checking conditions and loops has been fixed.

The syntax of the operators for checking a condition (implication) and of the loop has been changed and simplified, for which only one variant each has been left. The condition check is written as the mathematical consequence operator –>, and the loop with the help of the operator <->.

But to simplify the notation, one can use macros, in which case the condition checks and loops turn into the classical

\if(...){
    ...
} \elif(...) {
   ...
} \else {
   ...
};

or

 \while( ... ){ 
     ...
 }; 

The else construct of loop operators

Now the loop operator while supports the else construct, which is executed if the loop entry condition was not fulfilled. This behavior differs from the analogous constructs in the Python language, in which the else section is always executed, except when the loop is interrupted by break.

The else branch of a loop operator is written the same way as the otherwise branch in a conditional operator, i.e.

[ cond ] <-> {
   ...
}, [_] --> {
   ...
};

Or the same, but with the use of macros:

\while(cond) {
   ...
} \else {
   ...
};

Namespaces

Namespaces have been added to the NewLang syntax, in a style very similar to C++. Names are separated by a double colon, and to define a namespace it must be specified before the opening curly brace.

ns {
    name {
        var := 0; # The variable name will be ns::name::var
        ::var := 1; # A variable from the global namespace
    }
}

Program modules

The concept of program modules has been implemented — which repeats the concept of the hierarchical arrangement of files in the directory structure of the file system, as in the Python and Java languages.

The module name starts with the prefix @, and the directory structure is specified with a dot. Moreover, the concepts of program modules and namespaces are combined, and the full name of the variable from the previous paragraph will be @root.dir.module::ns::name::var, where root and dir are directories in the file system, and module is the file name.

Object-oriented programming

Part of the OOP concept has been implemented and support for defining classes and their inheritance has been added, which currently looks as follows:

:NewClass := :Class() { #  A new type (class)
    field := 1;
    method() := {};
};
obj := :NewClass(); # An instance of the class

Unexpectedly for myself I realized that, having a full set of variants of checks when creating objects (::= — create a new object, := — create a new one or assign a value to an existing one, = — only assign a value, and if the object does not exist there will be an error), the concept of overriding inherited functions requires no keywords at all:

:NewClass2 := :NewClass() { # A new class based on an existing one
    field ::= 2; # There will be an error, since the field field already exists in the base class
    method() = {}; # An analog of override, since method must exist in the base class
};

Interrupts, return and error handling

The ideology of returning from functions and error handling has been changed, or rather completely reworked. Now it is somewhat similar to the approach used in Ruby. Any sequence of commands enclosed in curly braces (including the function body) is considered a code block that has no special operator analogous to return, which returns some value. Simply any code block always returns the last computed value (this is somewhat similar to the “comma” operator in C/C++ languages).

In order to interrupt the execution of the code, two different commands are used — interrupts, which can conventionally be called the positive and negative result. Which approximately corresponds to the semantics of their notation. The “negative” interrupt is written as two minuses, and the “positive” interrupt as two pluses, i.e. -- or ++.

By default these operations return an empty value. For the interrupt to return a result, the returned data must be written between a pair of the corresponding symbols, i.e. -- 100 --, which is a close analog of the operator return 100; in other programming languages, and ++“String”++ — an analog of return "String";.

Although a more precise analog of these operators will still not be return, but throw, since these commands not only interrupt the execution of the sequence of commands in a block, but they can also be “caught”. For this, a code block with the corresponding semantics is used, {+ … +} — a code block that captures positive interrupts, and {- … -} — a code block that captures interrupts created by the – operators.

Such a concept (not explicitly separating returns from functions and exception handling), although it looks a little unusual, makes it possible to implement several very useful tricks that are difficult to implement in ordinary programming languages.

For example, returning from several nested functions without the need to handle the return result of each of them. In this example the function Test captures “positive” interrupts from nested functions:

    Test0(arg) := { \if($arg==0) \return("DONE - 0"); "FAIL" };
    Test1(arg) := { \if($arg==1) \return("DONE - 1"); Test0($arg); };
    Test(arg) := {+ \if($arg >= 0) Test1($arg); $arg; +};

    Test(0); # Will return "DONE - 0" — return from the nested function Test0
    Test(1); # Will return "DONE - 1" — return from the nested function Test1
    Test(2); # Will return "FAIL" — return from the nested function Test0
    Test(-2); # Will return -2 — return from the function Test

There is also the block {* … *}, which captures both types of interrupts. Such a code block supports the typing of the return value, which makes it possible to explicitly specify the data types that need to be captured. For example, {* ... *} :Type1 — an interrupt of the form ++ :Type1 ++ or --:Type1-- will be captured, which makes it possible to form the logic of the program’s operation very flexibly.

Code blocks with exception capture also support the otherwise operator (\else) which, by analogy with the \else operator in loops, is executed only if no interrupt occurred.

One can specify several types at once that need to be captured:

    {* 
        ....
    *} <:Type1, :Type2, :Type3>;

Binary build for Windows:

In the current release the clang version has been raised to 15, and the calls of native functions are again implemented with the help of libffi, which as a result made it possible to build a binary build not only for Linux, but also for Windows. And now one can download a ready-made binary.

Detailed information about the language:

Feedback

If you have suggestions for developing new or improving already existing functionality of NewLang, write.

Code examples

#!../output/nlc --eval
# Definition of the hello function
hello(str) := { 
    # Import of a standard C function
    printf := :Pointer('printf(format:FmtChar, ...):Int32');
    # Calling a C function with type checking of the arguments against the format string
    printf('%s', $str);  
    # Returning a value from the hello function
     $str;
};

hello('Hello, world!'); # Call the function</code>

Output (the first line is output by printf, and the second is the return value of the hello function):

    Hello, world!
    Hello, world!

Example script for computing factorial 1000

#!../output/nlc --eval

fact := 1\1;  # A rational number without precision limitation
mult := 1000..1..-1?; # Make an iterator from the range for the factors from 1000 to 2
[mult ?!] <-> {      # Loop until the iterator data runs out
    # Get the current factor and move to the next element of the iterator
    fact *= mult !; 
};
fact # Output the final result

Output:

402387260077093773543702433923003985719374864210714632543799910429938512398629 020592044208486969404800479988610197196058631666872994808558901323829669944590 997424504087073759918823627727188732519779505950995276120874975462497043601418 278094646496291056393887437886487337119181045825783647849977012476632889835955 735432513185323958463075557409114262417474349347553428646576611667797396668820 291207379143853719588249808126867838374559731746136085379534524221586593201928 090878297308431392844403281231558611036976801357304216168747609675871348312025 478589320767169132448426236131412508780208000261683151027341827977704784635868 170164365024153691398281264810213092761244896359928705114964975419909342221566 832572080821333186116811553615836546984046708975602900950537616475847728421889 679646244945160765353408198901385442487984959953319101723355556602139450399736 280750137837615307127761926849034352625200015888535147331611702103968175921510 907788019393178114194545257223865541461062892187960223838971476088506276862967 146674697562911234082439208160153780889893964518263243671616762179168909779911 903754031274622289988005195444414282012187361745992642956581746628302955570299 024324153181617210465832036786906117260158783520751516284225540265170483304226 143974286933061690897968482590125458327168226458066526769958652682272807075781 391858178889652208164348344825993266043367660176999612831860788386150279465955 131156552036093988180612138558600301435694527224206344631797460594682573103790 084024432438465657245014402821885252470935190620929023136493273497565513958720 559654228749774011413346962715422845862377387538230483865688976461927383814900 140767310446640259899490222221765904339901886018566526485061799702356193897017 860040811889729918311021171229845901641921068884387121855646124960798722908519 296819372388642614839657382291123125024186649353143970137428531926649875337218 940694281434118520158014123344828015051399694290153483077644569099073152433278 288269864602789864321139083506217095002597389863554277196742822248757586765752 344220207573630569498825087968928162753848863396909959826280956121450994871701 244516461260379029309120889086942028510640182154399457156805941872748998094254 742173582401063677404595741785160829230135358081840096996372524230560855903700 624271243416909004153690105933983835777939410970027753472000000000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000000000000000\1

How to view?