Reliable programming in the context of languages. Part 2 - Challengers

    The first part with functional requirements is here .

    Claimed as programming languages ​​with an eye on reliability.

    Alphabetically - Active Oberon, Ada, BetterC, IEC 61131-3 ST, Safe-C.

    At once, the disclaimer (excuse) is by no means an “all on the left side” campaign, and the review is rather academic - the language may not only have an actively supported modern development environment, but even a compiler for your platform.

    On the other hand, for the languages ​​in question there are open source compilers, and with the current level of software development - with interest, not too complicated syntax allows you to make a personal compiler and integrate into some kind of Eclipse with backlight and parser.

    As an indicator of the clarity of the language, I chose the implementation of Dijkstra's famous multi-threaded task about dining philosophers. Implementation is in the textbooks on the language and on the forums, which facilitated my work - it remains only to adapt. For example, a recent habr article about modern C ++ contains an implementation in C ++ 17 for comparison.

    Active Oberon (2004)


    It was created with an eye to the experience of Pascal, Modula, previous Oberons since 1988, Java, C #, Ada, as well as practical experience in application. It has an implementation in the form of OS A2 , which can act as runtime on top of * nix or Windows. Sources A2 and the compiler for the link .

    There is also an Oberon2 to C Compiler (OOC) project that is not tied to the Oberon environment. This is a slightly different dialect, the differences are described below.

    The key feature of Oberon is the exceptional brevity of the specification. These are 16 pages on the base Oberon-2 plus 23 pages on the multi-threaded Active extension.

    Simple and clear syntax that excludes obvious errors.

    Identifiers are case sensitive.

    OOP with objects on the heap with the garbage collector (GC).

    It differs from its predecessors in the more familiar OOP syntax in the form of Instance.Method (it used to be Method (Instance)) and support for multithreading with synchronization primitives.
    There is no dynamic dispatching in the OOP implementation, which can easily lead to a situation - they forgot to add processing for a new type.

    Streams can be assigned priority and high / realtime they are not interrupted by GC. Strings in the form of UTF-8 arrays.

    Rantime (Oberon System) provides interesting opportunities for restarting a failed procedure / module / thread in case of a runtime error - memory addressing, or, for example, integer overflow.

    The disadvantage is the lack of RAII, and convenient error handling - all through return codes, with the exception of the option below.

    Oberon-2 OOC


    It is more convenient for experiments, since Oberon does not require OS - it compiles in ANSI C and there are no interoperability problems. Differences from the Active version - there is no built-in multithreading language - instead there is a module for working with PThreads, but there is UTF16, hierarchical modularity and a system module for working with exceptions.

    Module 3


    There is also a relative from a slightly different development branch in the form of Modula-3. It was created on the basis of Oberon as opposed to the overdeveloped Ada. The implementation is here .

    Compared to Active Oberon, generics and exceptions are added, there are libraries for practical work with Unicode, GUI, and even Postgress. Simplified integration with C. Other multithreading semantics. RAII as WITH (similar to using in C #).

    But it seems that the development of Modula 3 stopped in 2010.

    Disclaimer. Having launched WinAOS, I ran into TRAPs (aka abort / stacktrace or runtime error) out of the blue - even the task manager does not work correctly, and although the system / runtime did not crash - but only the application, I had a certain doubt that reliability is determined by the language programming = (

    Also, AOC is sufficiently self-contained, with its approach to development.

    Source for Dining Philosophers
    MODULE Philo;
    (* Dining Philosophers Example from Active Oberon Language Report by Patrik Reali *)
    (* Adapted for running in AOS by Siemargl *)
    IMPORT Semaphores :=  Example8, Out;
    CONST
    	NofPhilo = 5; (* number of philosophers *)
    VAR
    	fork: ARRAY NofPhilo OF Semaphores.Semaphore;
    	i: LONGINT;
    TYPE
    	Philosopher = OBJECT
    		VAR
    			first, second: LONGINT;
    			(* forks used by this philosopher *)
    		PROCEDURE & Init(id: LONGINT);
    		BEGIN
    			IF id # NofPhilo-1 THEN
    				first := id; second := (id+1)
    			ELSE
    				first := 0; second := NofPhilo-1
    			END
    		END Init;
    		PROCEDURE Think;  (* Need lock console output *)
    		BEGIN {EXCLUSIVE}
    			Out.Int(first);	Out.String(".... Think....");	Out.Ln;
    		END Think;
    		PROCEDURE Eat;
    		BEGIN {EXCLUSIVE}
    			Out.Int(first);	Out.String(".... Eat....");	Out.Ln;
    		END Eat;
    	BEGIN {ACTIVE}
    		LOOP
    			Think;
    			fork[first].P; fork[second].P;
    			Eat;
    			fork[first].V; fork[second].V
    		END
    	END Philosopher;
    VAR
    	philo: ARRAY NofPhilo OF Philosopher;
    BEGIN
    	FOR i := 0 TO NofPhilo DO
    		NEW(fork[i], INTEGER(i));
    		NEW(philo[i], i);
    	END;
    END Philo.
    Philo.Philo1 ~
    

    Ada (1980, last valid 2016 standard)


    Actually, at first glance there is everything that I would like.

    And even a little more - there are numbers with exact floating-point calculations. For example, there is a realtime thread scheduler, cross-thread exchange, and a formally verified subset of the SPARK language. And much more.

    I think that if Ada’s reliability needed a damn horned one, it would be attached with instructions for calling in a difficult situation =)

    Implementation - GNUtaya Ada is developing, ISO / IEC standardized.

    The standard provides implementation with GC, but for compiled options it is often not implemented. Manual memory management is required - and here programmer errors are possible. However, the language is geared towards using the default stack and there is the concept of managed types with destructors. You can also define your GC implementation, auto-release, or reference counting for each data type.

    Ada Reference Manual 2012 contains 950 pages.

    Ada’s disadvantage, besides complexity, is its excessive verbosity, which, however, was conceived for the sake of readability. Due to the specificity of the language security model, integration with foreign libraries is difficult.

    The Ada-ru site has a good review translation article - the first link.

    Source for Dining Philosophers
    -- Code from https://rosettacode.org/wiki/Dining_philosophers#Ordered_mutexes
    -- ADA95 compatible so can run in ideone.com
    with Ada.Numerics.Float_Random;  use Ada.Numerics.Float_Random;
    with Ada.Text_IO;                use Ada.Text_IO;
    procedure Test_Dining_Philosophers is
       type Philosopher is (Aristotle, Kant, Spinoza, Marx, Russel);
       protected type Fork is
          entry Grab;
          procedure Put_Down;
       private
          Seized : Boolean := False;
       end Fork;
       protected body Fork is
          entry Grab when not Seized is
          begin
             Seized := True;
          end Grab;
          procedure Put_Down is
          begin
             Seized := False;
          end Put_Down;
       end Fork;
       Life_Span : constant := 20;    -- In his life a philosopher eats 20 times
       task type Person (ID : Philosopher; First, Second : not null access Fork);
       task body Person is
          Dice : Generator;
       begin
          Reset (Dice);
          for Life_Cycle in 1..Life_Span loop
             Put_Line (Philosopher'Image (ID) & " is thinking");
             delay Duration (Random (Dice) * 0.100);
             Put_Line (Philosopher'Image (ID) & " is hungry");
             First.Grab;
             Second.Grab;
             Put_Line (Philosopher'Image (ID) & " is eating");
             delay Duration (Random (Dice) * 0.100);
             Second.Put_Down;
             First.Put_Down;
          end loop;
          Put_Line (Philosopher'Image (ID) & " is leaving");
       end Person;
       Forks : array (1..5) of aliased Fork; -- Forks for hungry philosophers
                                             -- Start philosophers
       Ph_1 : Person (Aristotle, Forks (1)'Access, Forks (2)'Access);
       Ph_2 : Person (Kant,      Forks (2)'Access, Forks (3)'Access);
       Ph_3 : Person (Spinoza,   Forks (3)'Access, Forks (4)'Access);
       Ph_4 : Person (Marx,      Forks (4)'Access, Forks (5)'Access);
       Ph_5 : Person (Russel,    Forks (1)'Access, Forks (5)'Access);
    begin
       null; -- Nothing to do in the main task, just sit and behold
    end Test_Dining_Philosophers;
    


    BetterC (dlang subset 2017, original D - 2001, D 2.0 - 2007)


    The most modern implementation of the considered. The full description of the language is quite long - 649 pages - see the original site .

    Actually, this is the D language, but with restrictions with the -betterC switch. Why is that ?!

    Because the standard library D is Phobos, developed by Alexandrescu and turned out to be very cunning, completely built on templates. The key to this topic is that Phobos is uncontrollable in terms of memory consumption.

    The most important things that get lost in BetterC mode are multithreading, GC, strings, classes (structures remain - they are close in functionality - only on the stack) and exceptions (RAII and try-finally remain).

    It is possible, however, to write part of the program in full D, and the critical part in D-BetterC. There is also a system attribute function to control the non-use of dangerous effects: pure safe @nogc.

    Justification of the regime from the creator of the language.

    And then the squeeze - what is cut off and what remains available.

    Strings are contained in Phobos - and attempts to use them in BetterC result in infernal errors of instantiation of templates on elementary operations such as outputting a string to the console or concatenation. And in full D mode, the lines on the heap are also immutable, therefore operations with them lead to memory clutter.

    I had to meet complaints about bugs in the compiler several times. Which, however, is not surprising for a language competing in complexity with C ++. While preparing the article, I also had to face 4 errors - two occurred when trying to build dlangide with a new compiler and a couple when porting the philosopher problem (for example, crash when using beginthreadex).

    The mode has only recently appeared, and errors caused by the restriction of the BetterC mode get out already at the linking stage. To learn about this in advance, what features of the language are trimmed exactly - often have to do it first hand.

    Source for Dining Philosophers
    // compile dmd -betterC 
    import core.sys.windows.windows;
    import core.stdc.stdio;
    import core.stdc.stdlib : rand;
    //import std.typecons; // -impossible (
    //import std.string;  - impossible
    extern (Windows) alias btex_fptr = void function(void*) /*nothrow*/;
    //extern (C) uintptr_t _beginthreadex(void*, uint, btex_fptr, void*, uint, uint*) nothrow;
    /* Dining Philosophers example for a habr.com 
    *  by Siemargl, 2019
    *  BetterC variant. Compile >dmd -betterC Philo_BetterC.d
    */
    extern (C) uintptr_t _beginthread(btex_fptr, uint stack_size, void *arglist) nothrow;
    alias HANDLE    uintptr_t;
    alias HANDLE    Fork;
    const philocount = 5;
    const cycles = 20;
    HANDLE[philocount]  forks;
    struct Philosopher
    {
        const(char)* name;
        Fork left, right;
        HANDLE lifethread; 
    }
    Philosopher[philocount]  philos;
    extern (Windows) 
    void PhilosopherLifeCycle(void* data) nothrow
    {
        Philosopher* philo = cast(Philosopher*)data;
        for (int age = 0; age++ < cycles;)
        {
            printf("%s is thinking\n", philo.name);
            Sleep(rand() % 100);
            printf("%s is hungry\n", philo.name);
            WaitForSingleObject(philo.left, INFINITE);
            WaitForSingleObject(philo.right, INFINITE);
            printf("%s is eating\n", philo.name);
            Sleep(rand() % 100);
            ReleaseMutex(philo.right);
            ReleaseMutex(philo.left);
        }
        printf("%s is leaving\n", philo.name);
    }
    extern (C) int main() 
    {
        version(Windows){} else { static assert(false, "OS not supported"); }
        philos[0] = Philosopher ("Aristotlet".ptr, forks[0], forks[1], null);
        philos[1] = Philosopher ("Kant".ptr, forks[1], forks[2], null);
        philos[2] = Philosopher ("Spinoza".ptr, forks[2], forks[3], null);
        philos[3] = Philosopher ("Marx".ptr, forks[3], forks[4], null);
        philos[4] = Philosopher ("Russel".ptr, forks[0], forks[4], null);
        foreach(ref f; forks)
        {
            f = CreateMutex(null, false, null);
            assert(f);  
        }
        foreach(ref ph; philos)
        {
            ph.lifethread = _beginthread(&PhilosopherLifeCycle, 0, &ph);
            assert(ph.lifethread);  
        }
        foreach(ref ph; philos)
            WaitForSingleObject(ph.lifethread, INFINITE);
        // Close thread and mutex handles
        for( auto i = 0; i < philocount; i++ )
        {
            CloseHandle(philos[i].lifethread);
            CloseHandle(forks[i]);
        }
    	return 0;
    }
    


    For comparison, the source at full D .

    On the rosette you can also see options for other languages.

    IEC 61131-3 ST (1993, latest standard 2013)


    A niche programming language for microcontrollers. The standard implies 5 programming options, but writing an application for example in ladder logic is still an adventure. Therefore, we focus on one option - structured text.
    The text of the standard GOST R IEC 61131-3-2016 - 230 pages.

    There are implementations for PC / x86 and ARM - and commercial, the most famous of which is CODESYS (often also licensed with different names) and open - Beremiz - broadcast via C.

    Since there is integration with C, it’s quite possible to connect the libraries necessary for applied programming. On the other hand, in this area it is accepted that the logic rotates separately and only serves as a data server for another program or system - an interface with an operator or a DBMS that can already be written on anything - without realtime requirements or even any time in general ...

    Multithreaded programming for a user program appeared relatively recently - in microcontrollers this was not needed before.

    Type casting is mostly only explicit (relaxed in the latest standard). But overflow control is implementation dependent.

    In the latest edition of the standard, OOP appeared. Error handling is done by custom interrupt handlers.

    We can say that there is no dynamic memory allocation for the user. This historically happened - the amount of data processed by the microcontroller is always constant limited from above.

    Source (not verified)
    (* Dining Philosophers example for a habr.com 
    *  by Siemargl, 2019
    *  ISO61131 ST language variant. Must be specialized 4 ur PLC 
    * )
    CONFIGURATION PLC_1
    VAR_GLOBAL
    	Forks : USINT;
    	Philo_1: Philosopher;  (* Instance block - static vars *)
    	Philo_2: Philosopher;
    	Philo_3: Philosopher;
    	Philo_4: Philosopher;
    	Philo_5: Philosopher;
    END_VAR
    RESOURCE Station_1 ON CPU_1
    	TASK Task_1 (INTERVAL := T#100MS, PRIORITY := 1);
    	TASK Task_2 (INTERVAL := T#100MS, PRIORITY := 1);
    	TASK Task_3 (INTERVAL := T#100MS, PRIORITY := 1);
    	TASK Task_4 (INTERVAL := T#100MS, PRIORITY := 1);
    	TASK Task_5 (INTERVAL := T#100MS, PRIORITY := 1);
    	PROGRAM Life_1 WITH Task_1: 
    		Philo_1(Name := 'Kant', 0, 1, Forks);
    	PROGRAM Life2 WITH Task_2: 
    		Philo_2(Name := 'Aristotel', 1, 2, Forks);
    	PROGRAM Life3 WITH Task_3: 
    		Philo_3(Name := 'Spinoza', 2, 3, Forks);
    	PROGRAM Life4 WITH Task_4: 
    		Philo_4(Name := 'Marx', 3, 4, Forks);
    	PROGRAM Life5 WITH Task_5: 
    		Philo_5(Name := 'Russel', 4, 0, Forks);
    END_RESOURCE
    END_CONFIGURATION
    FUNCTION_BLOCK Philosopher;
    USING SysCpuHandling.library;
    VAR_INPUT
    	Name: STRING;
    	Left: UINT; 
    	Right: UINT;
    END_VAR
    VAR_IN_OUT
    	Forks: USINT;
    END_VAR
    VAR
    	Thinking:	BOOL := TRUE;  (* States *)
    	Hungry:	BOOL;
    	Eating:	BOOL;
    	HaveLeftFork:	BOOL;
    	TmThink:	TON;
    	TmEating:	TON;
    END_VAR
    	TmThink(In := Thinking; PT := T#3s);
    	TmEating(In := Eating; PT := T#5s);
    	IF Thinking THEN  (* Just waiting Timer *)
    		Thinking := NOT TmThink.Q;
    		Hungry := TmThink.Q;
    	ELSIF Hungry (* Try Atomic Lock Forks *)
    		IF HaveLeftFork 
    			IF SysCpuTestAndSetBit(Address := Forks, Len := 1, iBit := Right, bSet := 1) = ERR_OK THEN
    				Hungry := FALSE;
    				Eating := TRUE;
    			ELSE
    				RETURN;
    			END_IF
    		ELSIF
    			IF SysCpuTestAndSetBit(Address := Forks, Len := 1, iBit := Left, bSet := 1) = ERR_OK THEN
    				HaveLeftFork := TRUE; 
    			ELSE
    				RETURN;
    			END_IF
    		END_IF
    	ELSIF Eating  (* Waiting Timer, then lay forks *)
    		IF TmEating.Q THEN
    			Thinking := TRUE;
    			Eating := FALSE;
    			HaveLeftFork := FALSE;
    			SysCpuTestAndSetBit(Address := Forks, Len := 1, iBit := Right, bSet := 0);
    			SysCpuTestAndSetBit(Address := Forks, Len := 1, iBit := Left, bSet := 0);
    		END_IF
    	END_IF
    END_FUNCTION_BLOCK
    


    Safe-C (2011)


    Experimental C with the removal of dangerous chips and with the addition of modularity and multithreading. Project site
    Description about 103 pages. If you highlight the differences from C - very little, about 10 .

    Working with arrays and pointers is safe, dynamic memory with automatic reference counting - with double release checks and dangling links.

    The standard library has a minimal set of functions for the GUI, multithreading, network functions (including an http server).

    But - this implementation is only for Windows x86. Although the compiler and library code is open.

    As part of another research task, I put together a Web server layout that collects data from IoT sensors: a 75 Kb executive module, and a <1 MB partial memory set.

    Source for Dining Philosophers
    /* Dining Philosophers example for a habr.com 
    *  by Siemargl, 2019
    *  Safe-C variant. Compile >mk.exe philosafec.c
    */
    from std use console, thread, random;
    enum philos (ushort) { Aristotle, Kant, Spinoza, Marx, Russell, };
    const int cycles = 10;
    const ushort NUM = 5;
    uint  lived = NUM;
    packed struct philosopher // 32-bit
    {
    	philos	name;
    	byte left, right;
    }
    philosopher philo_body[NUM];
    SHARED_OBJECT forks[NUM];
    void philosopher_life(philosopher philo) 
    {
    	int age;
        for (age = 0; age++ < cycles; )
        {
            printf("%s is thinking\n", philo.name'string);
            delay((uint)rnd(1, 100));
            printf("%s is hungry\n", philo.name'string);
    		enter_shared_object(ref forks[philo.left]);
    		enter_shared_object(ref forks[philo.right]);
            printf("%s is eating\n", philo.name'string);
            delay((uint)rnd(1, 100));
    		leave_shared_object(ref forks[philo.right]);
    		leave_shared_object(ref forks[philo.left]);
        }
        printf("%s is leaving\n", philo.name'string);
        InterlockedExchange(ref lived, lived-1);
    }
    void main()
    {
    	philos i;
    	assert philosopher'size == 4;
    	philo_body[0] = {Aristotle, 0, 1};
    	philo_body[1] = {Kant, 1, 2};
    	philo_body[2] = {Spinoza, 2, 3};
    	philo_body[3] = {Marx, 3, 4};
    	philo_body[4] = {Russell, 0, 4};
    	for (i = philos'first; i <= philos'last; i++)
    	{
    		assert run philosopher_life(philo_body[(uint)i]) == 0;
    	}
    	while (lived > 0) sleep 0; // until all dies
    	for (i = philos'first; i <= philos'last; i++)
    	{
    		destroy_shared_object(ref forks[(uint)i]);
    	}
    }
    


    Finally - a summary table of compliance with functional requirements.
    Surely I missed or misinterpreted something - so correct it.

    Sources from the article on github .

    Also popular now: