Snippets

Gianluca Alloisio thoughts_on_programming_language

Created by Gianluca Alloisio last modified

Function arguments, returns, anonymous structs

Functions arguments and return define a type. Structs can be assigned partially with the square brackets operator.

f( s32 arg0, arg1; f32 arg2 = 1.0; { s32 * arr; u32 count; } arg3 )
-> { s32 ret0, ret1; Arr ret2; }
{
    for arg3.count -> i
    {
        arg3.arr[i] = round( f32( arg0 * i + arg1 ) * arg2 );
    }
    return { arg0 * arg3.count, arg2, Arr(arg3) };
}

Arr
{
    s32 * arr;
    u32 length;
}

Arr a = {};
s32 v;
[ v, s32 v1, a] = f( 3, 5, 1.2, a );
s32 vv;
[ vv = ret1, a = ret2 ] = f( arg1 = v, arg0 = 3, a );

f.args f_args = { arg0 = 2, arg1 = 3, arg2 = 5.5, arg3 = a };
f( f_args );
f_args.arg2 = 4.1;
f( f_args );

f.ret f_ret = f( f_args );

[ int foo = ret0, float bar = arg2 ] = f_args;

Function overloading makes functions to be identified by their argument type. They could not have the args type or they should be disallowed or:

f( float arg0, {int d, c; float a} arg1 ) -> void;
f( float arg0, int a ) -> void;

f( float,{int,int;} ).ret v0;

float val = 3.14;
int d = 2;
int c = 1;
float a = 3.0;
return_type_of( f( val,{d,c,a} ) ) v1;

auto v2 = f( val,{d,c,a} ) );

arg_type_of( f( val,{d,c,a} ) ) v3;
f( float,{int,int;} ).args v0;

Using

is a and has a

Foo
{
    int a[4];
    {
        float x,y;
    } b;
}

Bar
{
    using f;
    Foo f;
    float z;
}
Bar bar;
bar.b.x += 3;
bar.a[0] = 0;

BarBar
{
    using bar;
    Bar bar;
}
BarBar bb;
bb.b.y = 1;

FooBar
{
    using bar;
    Foo foo;
    Bar bar;
}
FooBar fb;
fb.b.x = 0;
fb.bar.x = 1;
assert( fb.b.x != fb.bar.x );

FooBarErr
{
    using bar;
    using foo;
    Foo foo;
    Bar bar;
    /*
     error: `using foo;` makes names collide with bar
    */
}

void
m( Foo f )
{
    global_something.x = f.x;
}

float
get_x( Foo f )
{
    return f.x;
}

void
normalize( Foo * f )
{
    normalize( &f.b );
}

void
normalize( {float x,y} * )
{
    float l = sqrt( x*x + y*y );
    x /= l;
    y /= l;
}

Foo foo;
foo.m();
float x = foo.get_x();
foo.normalize();
normalize( &foo ); // the user takes the address
normalize( foo ); // the compiler takes the addres

Bar bar;
bar.m();
float x = bar.get_x();
bar.normalize()
normalize( bar ); // the compiler passes the address of bar.f
normalize( &bar ); // error
normalize( &bar.f ); // this is ok



void
normalize( {float x,y} * )
{
    float l = sqrt( x*x + y*y );
    x /= l;
    y /= l;
}
void
normalize( {float x,y} * v )
{
    using v;

    float l = sqrt( x*x + y*y );
    x /= l;
    y /= l;
}
void
normalize( using {float x,y} * v )
{
    float l = sqrt( x*x + y*y );
    x /= l;
    y /= l;
}
void
afoo({ float arg0; int arg1, arg2; {float a,b;} }*)
{
    arg0 = a * b + arg1 / arg2;
}
float a = 0;
afoo( a, 3, 5, {1.3,2.1} );
assert( a == 1.3*2.1 + 3/5 );
afoo( 9, 3, 5, {1.3,2.1} ); // this is ok, could generate a warning. Could be optimized out.

Metaprogramming

Functions

Type specializations

fun({ %T arg0 }) -> %T
{
    %T foo = arg0 * 3;
    return foo;
}
f32 v = fun( 3.14 );

The compiler generates the specialization of fun with %T equal f32:

fun({ f32 arg0 }) -> %T
{
    f32 foo = arg0 * 3;
    return foo;
}
fun({ %T0 arg0 }) -> %T1
{
    return arg0.m;
}
Foo
{
    f32 m;
    s32 n;
}
Bar
{
    f32 n, p;
}
Foo f = {0, 0};
auto p = fun( f );

The return type T1 is deduced from the return expression, in this case f32. Foo is accepted as %T0 because all expressions of fun are valid. e.g. arg0.m is valid, passing a Bar would end into a compiler error because Bar has no m member. The generated specialization is:

fun({ Foo arg0 }) -> f32
{
    return arg0.m;
}

Type Constraints

foo({ %T0 arg0, %T1 arg1 }) -> %T2
# %T0 != %T1
{
    return 3.14 * arg0 + arg1;
}

bar({ %T0 * arg0, %T1 * arg1 }) -> void
# sizeof( %T0 ) == sizeof( %T1 )
{
    memcpy( arg0, arg1, sizeof( arg0 ) );
}

Entity
{
    u64 id;
    u16 type;
}
register({ %T0 * p }) -> bool
# %T0 can_be_a Entity
{
    // ...
}
Player
{
    using e;
    Entity e;
    V2 pos;
    u8 health;
}
Player p;
bool res = register({ p });
assert( res );

Condition Specializations

fun({ f32 arg0, b32 arg1 }) -> void
# specialize arg1
{
    // ...
}

/*A*/ fun({ 3.14, false });
/*B*/ fun({ 3.14, true )};
/*C*/ fun({ 3.14, 10 )};
b32 b = get_bool_from_some_runtime_dependent_place();
/*D*/ fun({ 3.14, b )};

In cases A and B two different versions of fun get called. In case B and C the same version of fun gets called, because 10 evaluates as true. In case D a nonspecialized version of fun gets called, because the value of b cannot be known at runtime.

fun({ f32 arg0 }) -> void
# specialize arg0 > 0 else
{
    if ( arg0 > 0 )
    {
        // ...
    } else
    {
        // ...
    }
}
bar({ f32 arg0 }) -> void
{
    if ( arg0 > 0 )
    # specialize
    {
        // ...
    } else
    {
        // ...
    }
}
barbar({ u32 arg0 }) -> void
{
    switch ( arg0 )
    {
        case FOO:
        # specialize
        // ...
        case BAR:
        // ...
        default:
        // ...
    }
}
barbar({ u32 arg0 }) -> void
{
    switch ( arg0 )
    # specialize
    {
        case FOO:
        // ...
        case BAR:
        // ...
        default:
        // ...
    }
}

Data Structures

Dynamic_Array_%T
{
    %T  array;
    u32 capacity,
        occupancy;
}

Dynamic_Array_f32 df = {};
df.capacity = 16;
alloc( df.array, df.capacity );

This type of naming could lead to ambiguities.

Hash_Table_%T_%l
{
    Node
    {
        %T     data;
        Node * next;
    }
    Node table[%l];
}
Hash_Table_s32_128 ht = {};

Not only types could be used as specializing, but numbers too.

%T_Hash_Table_%l
{
    Node
    {
        %T     data;
        Node * next;
    }
    Node table[%l];
}
s32_Hash_Table_128 ht = {};

This naming type enables any kind of naming structure. One can put specializings wherever she wants in the typename.

Some_Structure_of_%l_%T
# %TId = minimum_unsigned( %l )
{
    Address
    {
        %TId id;
        u32 type;
    }
    %T data[%l];
}

Compiler functionalities lets you specialize with conditions, so you can easily optimize your code.

Macroy ?

I'm just thinking out loud texty.

# for 5 -> $i
# {
    stuff[$i] = {};
    stuff[$i].foo = ## some_fun( $i ) ##;
# }

###
some_fun( s32 i ) -> f32
{
    f32 ret;

    // ...

    return ret;
}
###

I usually use long macros when I need a repeated snippet of code to run and to be able to return or break from where I'm calling it. E.g.:

bool
some_function( int arg )
{
    #define SNIPPET(a,b,c)                  \
        {                                   \
            int tmp = other_fun( b, a, c ); \
            Foo f = {a,tmp};                \
            do_stuff( &f );                 \
            if ( !register( f ) )           \
            {                               \
                return false;               \
            }                               \
        }                                   \

    SNIPPET( "Thomas", 33, arg     )
    SNIPPET( "Alicia", 15, arg * 3 )
    SNIPPET( "Emma"  , 52, arg     )
    /*
        .
        .
        .
    */
    return true;
}

I could declare an inline function, but I'd still need a macro for the check.

inline
bool
snippet( const char * a, int b, int c)
{
    int tmp = other_fun( b, a, c );
    Foo f = {a,tmp};
    do_stuff( &f );
    if ( !register( f ) )
    {
        return false;
    }
    return true;
}

bool
some_function( int arg )
{
    #define SNIPPET( a, b, c ) if ( !snippet( a, b, c ) ) { return false; }

    SNIPPET( "Thomas", 33, arg     )
    SNIPPET( "Alicia", 15, arg * 3 )
    SNIPPET( "Emma"  , 52, arg     )
    /*
        .
        .
        .
    */
    return true;
}

An alternative is to define tables of data and to iterate on them.

bool
some_function( int arg )
{
    const char * names[] = { "Thomas", "Alicia", "Emma", /* ... */ };
    int n[] = { 33, 13, 52, /* ... */ };
    int m[] = { arg, arg * 3, arg, /* ... */ };
    for ( int i = 0; i != ARRAY_COUNT( n ); ++i )
    {
        const char * a = names[i];
        int          b = n    [i];
        int          c = m    [i];

        int tmp = other_fun( b, a, c );
        Foo f = {a,tmp};
        do_stuff( &f );
        if ( !register( f ) )
        {
            return false;
        }
    }
    return true;
}

This could be more robust and easy to work with using a custom struct, but defining it is annoying because you can't define it locally in the function.

struct
Bar
{
    const char * name;
    int n,m;
};
bool
some_function( int arg )
{
    Bar bar[] = {
        { "Thomas", 33, arg     },
        { "Alicia", 13, arg * 3 },
        { "Emma"  , 52, arg     },
        /*
            .
            .
            .
        */
    };
    for ( int i = 0; i != ARRAY_COUNT( foo ); ++i )
    {
        const char * a = bar[i].name;
        int          b = bar[i].n;
        int          c = bar[i].m;

        int tmp = other_fun( b, a, c );
        Foo f = {a,tmp};
        do_stuff( &f );
        if ( !register( f ) )
        {
            return false;
        }
    }
    return true;
}

So maybe being able to declare structs in funtions could solve this problem? Together with a nice iteration construct.

some_function({ s32 arg }) -> bool
{
    _
    {
        const char * name;
        int n,m;
    } bar[] = {
        { "Thomas", 33, arg     },
        { "Alicia", 13, arg * 3 },
        { "Emma"  , 52, arg     },
        /*
            .
            .
            .
        */
    };
    for length( bar ) -> i
    {
        const char * a = bar[i].name;
        int          b = bar[i].n;
        int          c = bar[i].m;

        int tmp = other_fun( b, a, c );
        Foo f = {a,tmp};
        do_stuff( &f );
        if ( !register( f ) )
        {
            return false;
        }
    }
    return true;
}
some_function({ s32 arg }) -> bool
{
    _
    {
        const char * name;
        int n,m;
    } bar[] = {
        { "Thomas", 33, arg     },
        { "Alicia", 13, arg * 3 },
        { "Emma"  , 52, arg     },
        /*
            .
            .
            .
        */
    };
    for bar -> e
    {
        const char * a = e.name;
        int          b = e.n;
        int          c = e.m;

        int tmp = other_fun( b, a, c );
        Foo f = {a,tmp};
        do_stuff( &f );
        if ( !register( f ) )
        {
            return false;
        }
    }
    return true;
}

A general solution could be to have something like typed macros, or localized functions. I'll call them snippets.

some_function({ int arg }) -> bool
{
    snippet s( char * a, s32 b, s32 c)
    {
        s32 tmp = other_fun( b, a, c );
        Foo f = {a,tmp};
        do_stuff( &f );
        if ( !register( f ) )
        {
            return false;
        }
    }

    s( "Thomas", 33, arg     )
    s( "Alicia", 15, arg * 3 )
    s( "Emma"  , 52, arg     )
    /*
        .
        .
        .
    */
    return true;
}

A snippet will not be called as a function, but will be embedded, like a macro, but it will have type checks on the arguments. It has it's own block, but being embedded it also inherits the outer context, so it can refer to a variable a declared before the snippet is embedded.

f({}) -> void
{
    snippet foo( s32 num, f32 * ptr )
    {
        for num -> i
        {
            ptr[i] = fun( i, bar );
            bar += 1.0;
        }
    }
    f32 * arr = alloc( 32, f32 );
    f32 * arr2 = alloc( 64, f32 );
    f32 bar = 0.0f;
    foo( 32, arr );
    foo( 64, arr2 );
}

XMacroy?

#define BAR_ENUM(a,b) a,
#define BAR_STRING b,
#define BAR_ELEMENTS(ENTRY) \
    ENTRY(DOLLAR,"$") \
    ENTRY(EURO,"€") \
    ENTRY(POUND,"£") \

enum
Bar_en
{
    BAR_ELEMENTS(BAR_ENUM)
}
const char ** bar_strings[] = {
    BAR_ELEMENTS(BAR_STRING)
};

XMacros are very useful in C when you have an enum and you need to print it, aka convert its value to a string. You could also use them for structs.

#define BAR_STRUCT(a,b,c) a b;
#define BAR_STRUCT_INITIALIZED(a,b,c) a b = c;
#define BAR_INITIALIZE(a,b,c) this->b = c;
#define BAR_ELEMENTS(ENTRY) \
    ENTRY(int, a, 3) \
    ENTRY(float, b, 2.32) \
    ENTRY(struct Bar *, c, NULL) \

struct
Bar
{
    BAR_ELEMENTS(BAR_STRUCT)
}
void
bar_init( Bar * this )
{
    BAR_ELEMENTS(BAR_INITIALIZE)
}

You could also do more advanced stuff, like taking offsets, using them to associate a string to each member, etc. One useful application is automatic serialization/deserialization.

Once you become familiar with them they are a powerful tool. The problem is that the code becomes not that readable to you and not readable to a third person. Having all the code and data relative to a member in the same place is useful, and helps readability and maintainability.

The problems xmacros solve that I've identified so far:

  1. printable enums
  2. SOA <--> AOS declaration and conversion
  3. automatic serialization/deserialization
  4. jumptables of enums
  5. struct members initialization code next to the member declaration.

More generally defining something in a place and being able to write all the relative code next to the initialization. Code parts that are called in other places.

One possibility to ebable all that is to provide hooks on AST level, like in Jai. It's very powerful, but code parts get scattered all around or get dissolved in meta-code. You could generate a function to retrieve the stringified version of an enum member, or to manage serialization/deserialization. Adding code for jumptables or inizializations could be harder. Being able to work with code parts as data structures in the meta-code could make things easy to write and to use (meaning meta-code writing and code using).

###
Entity
{
    Name   enum_name;
    String printable_name;
    String printable_description;
    Code   initialization_code;
    Code   update_code;
    Code   data;
}

Entity entity_types[] = {
    {
        BALL, "ball", "A ball. Spherical one, you know?",
        {
            this->phys = {};
            this->m = 0.3;
            this->radius = 0.15;
        },
        {
            phys_update( this->phys );
        },
        {
            Phys phys;
            f32 m;
            f32 radius;
        }
    },
    /*
     .
     .
     .
     */
};
###

/*
 .
 .
 .
 */

enum
Entity_Type
{
}

Constructs

while

ifwhile ( some_condition() )
{
    do_stuff();
} else
{
    do_other_stuff();
}

That whould differentiate from while, it has pros and cons. Being able to add an else after a while as you do with ifs is very convenient. Having more keywords weights on the brain, making reading code a bit diffucult, leaving less brain for the comprehension of the code and so more bugs that get undetected.

This is cleaner.

if ( a )
{
    do();
} else
while ( some_condition() )
{
    do_stuff();
} else
{
    do_other_stuff();
}

while ( b )
{
    // ...
}

for

This should iterate i from 0 to a.count-1.

for a.count -> i
{
    printf( "a[%2d] = %d", a.arr[i] );
}

If you'd want to specify the starting point:

for 0..a.count -> i
{
    printf( "a[%2d] = %d", a.arr[i] );
}

What about the type? Defaulting to the same of the range or else specifying it like:

for a.count -> s32 i
{
    printf( "a[%2d] = %d", a.arr[i] );
}

This is more classic but I think the range expression is not enough visually separated.

for s32 i = 0 -> a.count
{
    printf( "a[%2d] = %d", a.arr[i] );
}

Using square brackest separates it more. The right member would be excluded from the range.

for s32 i = [ 0 -> a.count ]
{
    printf( "a[%2d] = %d", a.arr[i] );
}

It could generate confusion with math style brackets/parenthesis for ranges, so maybe:

for s32 i = [ 0 -> a.count )
{
    printf( "a[%2d] = %d", a.arr[i] );
}

Sometimes one does not want to iterate by one, but by more.

for s32 i = [ 0 -> a.count ), 3
{
    printf( "a[%2d] = %d", a.arr[i] );
}

Sometimes one iterates backwards. In this case should the sign of the step be derived by the compiler or specified by the user? In the latter case this would iterate up to overflow. Could this be a wanted behavior? Could the behavior of overflow be fixated for the language or for the hardware?

for s32 i = [ a.count -> 0 ), 3
{
    printf( "a[%2d] = %d", a.arr[i] );
}

What about iterating in a range that is not known at compile time? What sign should be the step? Should the compiler add a check to determine it?

for s32 i = [ a.count -> min_i ), 3
{
    printf( "a[%2d] = %d", a.arr[i] );
}

Maybe the programmer could specify the sign as +3/-3 or leave it unspecified as 3. The compiler would add a runtime check only in the second case.

for s32 i = [ a.count -> min_i ), +3
{
    printf( "a[%2d] = %d", a.arr[i] );
}

More mathy, using comma instead of arrow?

for s32 i = [ a.count, min_i ), +3
{
    printf( "a[%2d] = %d", a.arr[i] );
}

Double dots are often used in pseudocode.

for s32 i = [ a.count .. min_i ), +3
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for s32 i = a.count .. min_i, +3
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for a.count .. min_i, +3 -> s32 i
{
    printf( "a[%2d] = %d", a.arr[i] );
}

Hmm. What is more important, the range or the variable? Or the left to right readability like a phrase?

for s32 i in [ a.count .. min_i ) by +3
{
    printf( "a[%2d] = %d", a.arr[i] );
}

What about geometric iterations?

for s32 i in [ a.count .. min_i ) by *3
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for s32 i in [ a.count .. min_i ) by *-3
{
    printf( "a[%2d] = %d", a.arr[i] );
}

Multiple ranges?

for s32 i in [ a.count .. min_i ):[0..3] by *-3
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for s32 i in [ a.count .. min_i ) by *3 : [0..3] : [ min_i .. a.count ) by 2
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for s32 i in [ a.count .. min_i ) by *3, [0..3], [ min_i .. a.count ) by 2
{
    printf( "a[%2d] = %d", a.arr[i] );
}

Keeping track of iteration count? Rust has .enumerate() but I don't like it because you have to write it as a method and because counter and index are defined only by order of declaration. At least I could drop the .enumerate().

for {s32 i, counter} in [ a.count .. min_i ) by *3 : [0..3] : [ min_i .. a.count ) by 2
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for {s32 i; u8 counter} in [ a.count .. min_i ) by *3 : [0..3] : [ min_i .. a.count ) by 2
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for [ s32 i; u8 counter ] in [ a.count .. min_i ) by *3 : [0..3] : [ min_i .. a.count ) by 2
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for [ a.count .. min_i ) by *3 : [0..3] : [ min_i .. a.count ) by 2 -> s32 i; u8 counter
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for [ a.count .. min_i ) by *3 : [0..3] : [ min_i .. a.count ) by 2 -> i
{
    printf( "a[%2d] = %d", a.arr[i] );
}

The math notation for ranges could confuse non mathy programmers... :\

for [ a.count .. | min_i ] by *3 : [0..3] : [ min_i .. | a.count ] by 2 -> i
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for [ a.count .. min_i ] by *3 : [0..3] : [ min_i .. a.count ] by 2 -> i
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for [ a.count |..| min_i ] by *3 : [0..3] : [ min_i | .. | a.count ] by 2 -> i
{
    printf( "a[%2d] = %d", a.arr[i] );
}

Maybe...

for [ a.count .*3. min_i ] : [0..3] : [ min_i .2. a.count ] -> i
{
    printf( "a[%2d] = %d", a.arr[i] );
}
for [ a.count |.*3.| min_i ] : [0..3] : [ min_i | .2. | a.count ] -> i
{
    printf( "a[%2d] = %d", a.arr[i] );
}

OK, no.

loops

I'd like a loop with these phases

- loop_keyword
- boot     : executed before all, in the same context, so you can declare variables
- check    : loop check, if fails the epilogue is executed
- advance  : executed between iterations and after continue
- epilogue : executed after the loop has failed
- body     : the iteration code

What if you break? Is the epilogue executed? Maybe a new keyword to break over the epilogue? Or maybe a break value that gets evaluated in the epilogue? Having a value and evaluating it would mean having a check and having to optimize it out in compile optimization. Maybe I could use the same syntax of functions, with named parameters.

    loop ( {}, {}, {}, {}, {} )
    loop ( .boot={}, .check={}, .advance={}, .epilogue={}, .body={} )
    loop
    (
        .boot=
        {
        },
        .check=
        {
        },
        .advance=
        {
        },
        .epilogue=
        {
        },
        .body=
        {
        }
    )
    loop
    (
        ^boot
        {
        },
        ^check
        {
        },
        ^advance
        {
        },
        ^epilogue
        {
        },
        ^body
        {
        }
    )
    loop
    (
        boot
        {
        },
        check
        {
        },
        advance
        {
        },
        epilogue
        {
        },
        body
        {
        }
    )
    loop
    (
        boot
        {
        }
        check
        {
        }
        advance
        {
        }
        epilogue
        {
        }
        body
        {
        }
    )
    loop
    (
        {
        }
        {
        }
        {
        }
        {
        }
        {
        }
    )
    loop
    (
        boot
        {
            Node * node = l->first;
        }
        check
        (
            node
        )
        advance
        {
            go_ahead();
        }
        epilogue
        {
            closing_down();
        }
    )
    {
        do_the_stuff();
    }

Comments (0)

HTTPS SSH

You can clone a snippet to your computer for local editing. Learn more.