I've been working on implementing the grammar for the Del language over the last few days and I think that I have the base language fleshed out (save the preprocessor directives) and I want to write an overview for two reasons. The first reason is by overviewing and explaining the language I might catch something I missed. The second reason is that I want to have a nice post to go over the syntax rules of the language as I see them now for later reference.

Units

Units are blocks of code that help logically differentiate functionality. They are similar to namespaces in C++ in that they scope their components to a name. The primary difference being that everything in Del must exist within a unit, and 'main' will be semantically enforced to only exist once anywhere throughout the program no matter what unit or sub-unit it exists in.

Note: Units can not exist within functions or objects.

Example :

unit my_program {

    unit sub_unit {
    
    }
    
    unit other_sub {
    
        unit sub_sub {
        
        }
    }
}

Units can have sub units and sub-units can have sub-units. At the time of this post I don't intend on enforcing a depth limit, but I might enforce a maximum depth of some arbitrary number like 64.

Accessing something in a subunit will occur with the unit name prefixed onto a variable separated with a '.' to get something similar to the form :

    my_program.sub_unit.variable = 3;

Of course this snippit is 'wrong' as there is no 'variable' in sub_unit, but thats because I haven't covered variables yet. It is pseudo-code.

Variables / Types / Functions / Calls

I want to group these things together because they are all required to demonstrate functions.

Variables

There are two primary ways to create a variable. Using "let" and "var". These differentiate immutable and mutable definitions respectively.

    let <variable> : <type> = <data>;
    var <variable> : <type> = <data>;

A note about creating variables

Variables must have a default value on creation. I don't allow non-assignment of variables anywhere. This is a personal preference as uninitiated variables are evil, and must be stopped at all costs.

Types

The base "types" in Del are :

  1. int
  2. double
  3. string
  4. object

int

Integers don't have a specific restriction on their size based on the grammar, but with NVM being its primary target they will almost assuredly be 32-bit signed integers. Supporting bigger numbers will be an extension of the language in the future.

var mutable_int   : int = 4; 
let immutable_int : int = 4; 

double

Similar to integers, the grammar doesn't dictate how the NVM should support doubles, but since doubles are something native to NVM I suspect they will be a 1-to-1 representation in the NVM.

var mutable_dbl   : double = 4.0; 
let immutable_dbl : double = 4.0; 

string

Strings suck. They are a menace, but nevertheless they are required. Strings in Del can be of any arbitrary length upon initiation. The '+' operator is supported for concatenating strings. Adding non-string types to strings will not be natively supported by Del at first, at least not by grammar. I intend on writing a library in Del for string operations.

var mutable_str   : string = "I am a string"; 
let immutable_str : string = "I am also a string"; 

object

The fanciest of the types, that will need a bit of explaining. Objects won't support inheritance.. at least not for a while. Objects can be seen more as a means to logically group data together and enforce access rights to callers.

object my_obj {

        pub {
            int: some_x;
            int: some_y;
        }

        priv {
        }
    }

As can be seen by the above, an object definition starts with the keyword "object" followed by the object name. Inside the object are "pub" and "priv" blocks; These blocks will differentiate access to public and private parts of the object relative to whose calling the object. In an object definition only "pub" is mandatory. All definitions for an object (functions/ variables/ etc) must be within one of these blocks.

As you might be able to tell, when indicating a variable in an object, you don't designate them like you would standard variables. This is because we aren't creating variables, we are describing variables that will be there when the object is created.

Instantiating an object :


let immutable_oject : object<my_obj>  = { some_x = 0; some_y = 0; };
var mutable_oject   : object<my_obj>  = { some_x = 0; some_y = 0; };

Accessing an item inside the object is similar to accessing something in a unit. You will prefix the variable name with the object name like so :

mutable_object.some_x = 3;

So what about objects inside objects?

object<other_obj> : some_obj;

Adding the above to the object's pub or priv section will allow an object to be put inside an object. However, unlike other languages since Del doesn't allow uninstantiated variables, and doesn't have a user-facing concept of a pointer, an object can not hold a type of its self. Now I know, a lot of things get a bit harder to implement with this rule but its not a game stopper. This will be semantically enforced.

What about the instantiation of private member variables?

Good question - Inside a priv block, if any member variable exists an "init" function will be expected that must initialize all member variables.

Here is an example object that is fully formed :
(It contains a function... being described next)

    object my_obj {

        pub {
        
            // These items will be expected to be instantiated by the
            // person who creates the object
            int: some_x;
            int: some_y;
            object<other_obj> : some_obj;
        }

        priv {
        
            int: private_int;
            
            // This will be called immediately after the instantiation
            func init() -> nil {
                
                private_int = 0;
                return;
            }
        }
   }

Functions

Functions in Del take on a form similar to the following :

    func <name> -> <type> {
    
        return <type>;
    }

A function's return type can be any of the types described above, or a special type only used in describing returns : "nil". This type was added to explicitly indicate that the function isn't returning anything, and anything that attempts to use a nil-returning object for a variable instantiation will cause the compilation to fail. The semantic analysis step will put a stop to this.

All functions will be enforced to have a 'return' at the end that matches the stated return type.

Examples :


func double_return() -> double {

    return 3.14159;
}

func no_return() -> nil {

    return;
}

func string_return() -> string {

    return "A String";
}

func int_return() -> int {

    return 24;
}


func object_return() -> object<my_obj> {

    // Objects being returned must first be assigned 
    // to a variable for now
    // This is by design for clarity of implementation

    var obj : object<my_obj> = {};
    return obj;
}

Function Parameters / Calling Functions

Function parameters are inherently pass by value. Thins will be copied and sent unless explicitly told to do so.. twice.. technically. Before we get into references, here is a basic example of a function with parameters:

func params(int: x, string: z) -> nil  {

    return;
}

As you can see, parameters take on a similar description as they do in object definitions using <type>:<name>.

Now, what about references, and whats with the "twice" comment ?
A function taking references needs an "&" prefixed on the type :

func as_refs(&object<my_obj> : obj, &int: x, double: y) -> nil {

    return;
}

In the above function you can see the first 2 parameters are references, while the last is not.

The reason I said "twice.. technically" is because when calling a function, the call must also indicate that we are passing a reference. The reason this is enforced is to help clarify whats happening in the code to someone who is reading it. It has always bothered me when you have to check the implementation of the function being called to see if it is taking a value or reference. Here are some call examples :

var an_int : int = 0;

as_refs(&an_obj, &an_int, 3.14);

// Or In an expression! 

var some_double : double = 3.0 + double_return();

More about references

Now that you know references exist, you might wonder if you can set a variable as a reference to another variable. The simple answer is a big'ol NO. References are only for describing variable passing mechanics. If you create a variable in a function based on a parameter that exists as a reference, you will receive a copy. This is done intentionally to make simple the implementation of references and to limit the user to doing things considered "generally safe". If someone has a problem with mu reasoning here, please feel free to reach out. We can discuss this.

Flow Control

Del has a couple different means of flow control "if/elif/else" statements, and "while/for/named" loop statements.

If statements

The if statements of Del are pretty standard:

var first  : int = 0; 
var second : int = 1; 

if(first) {

} elif (second) {

    var moot : int = 33; 

} elif (0) {

} else {

}

Starts with an "if" there can be any number of follow-up "elif" statements and an optional "else" catch-call statement.

Loops

while(1) {
    cont;
}

loop ' my_loop {
    cont;
    break my_loop;
}

for(var x : int = 4; x < 10; x = x + 1; ) {

    cont;
}

Loops in Del are mostly standard with the exception of the named loop (the second loop) which we'll get to in a moment.

With loops in del you can use the "cont" keyword inside a loop to "continue" similar to how you can "continue" in C++ loops. The above example shows that you can use them in all loops, but you don't need to have it present in a loop. Its optional.

In the "named" loop you can see there is something funky happening. There is no condition, and there is a "break" statement. Whats going on here?
Named loops are exactly that, a loop with a name.

loop ' <name> {

    break <name>
}

You define the loop with a name that has to be unique within the context that the loop exists, and that loop will run until a "break" statement occurs with a matching name. This allows nested loops to be broken specifically from different contexts.

Example :

loop 'outer {


    loop ' inner {
    
        if(some_condition){
            break outer;
        }
    }

}

Dynamic Memory / Memory Management

One thing I wanted to avoid in the Del language is the need to monitor memory usage too closely, and I wanted to do away with pointers. Nobody likes pointers, not even those of us who use them at their day job an aren't afraid of them. If you say you like pointers, you're lying to yourself.

So how is memory managed?

When something goes out of scope, its freed. Thats a simple way of killing things off that doesn't cause people too much heartache. So what about things that need to stick around? Units. This is one of the beauty of units. If we plop something in a unit, it will stick around until the end of the program. So how do we delete allocated things? You don't. If you want something you have it for life. . . That is unless you use a "dyn" object.

Whats a dyn? Its a special type of variable that doesn't interact with other variables with the exception of using them to set and get data. I'll first plop in an example of a dyn and then break it down :


dyn::create(my_dyn, type: int);

dyn::expand(&my_dyn, value: 10);

dyn::insert(&my_dyn, index: 12, value: 44);

The first line instantiates something we can use for dynamic memory, here its called "my_dyn" and it can hold "int" types.

The second line we expend the dyn, passing the "my_dyn" as a reference, and asking to expand to a size of "10" giving us space for 10 integers.

Note: Expanding does not create integers, it is similar to the C++ vector 'reserve' method.

The third is actually something that you might think is an error. We are asking to insert the value 44 in index 12, which shouldn't exist. It doesn't. So dyn will expand the storage to fulfill the request as long as there is memory to do so. This parameter can be a literal integer, string, double or a variable. To place an object, a variable of a constructed object must be given.

If memory runs out, it is up to the implementation of the language to handle if it should die, or see if it can make more room. What specifically will happen for the NVM implementation is not yet decided.

Here are the remaining operations for Dyn. I will comment this code block, but it should be pretty self explanatory:


// Clear all memory and shrink size
dyn::clear(&my_dyn);

// Delete a specific item and resizes dyn
dyn::delete(&my_dyn, index: 4);

// Get an item at a particular index - If the item does not exist
// The item handed by reference should be unchanged
var ret_item : int = 0;
dyn::get(&my_dyn, index: 4, dest: &ret_item);

// Retrieve the current size of the dyn (how many elements it encompases)
var current_size : int = 0;
dyn::size(&my_dyn, dest: &current_size);

// Get the front-most item in the dyn
dyn::front(&my_dyn, dest: &ret_item);

// Get the end-most item in the dym
dyn::back(&my_dyn, dest: &ret_item);

Of course, once things get along the way real documentation will be presented that describe all of the minute details for the operations here, this is just a brief overview.

Wrapping Up

So I'm really glad I did this overview. I found a mistake in the grammar that allowed units to be made in functions... which is now fixed. Everything seems alright and I can't think of anything else that needs to be added to the grammar for now.

I'm sure as I go to implement everything else I'll find something that will need to be updated, but for now this is the Del Grammar. I think this should suffice for the time being; once the language is being put together and things are nearing the first completion I will write another post similar to this one, and then some honest documentation.