Skip to content

Spitfire1970/compilers_flow_public

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

31 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

My language is called Flow.

Motivation

Inspiration

My idea is inspired by AI hackathons where participants are expected to build projects around LLMs. The crux of these projects is getting different models and their libraries to work together. Tools like LangChain exist out there but I wanted to create a dedicated programming language that makes the AI part of these wrapper-style projects more trivial to implement.

Use case, caveats and potential

Flow isn't ground-breaking, but assuming a decent backend, it provides an easier syntax for performing specific kinds of tasks which leads to more efficient programmers.

Flow has in-built data types for different modalities like Audio, Text, Image and Video and also provides a Dir data type. It's arrow syntax ("<-", the flow operator) makes it easy to follow the control flow of execution. Flow is meant for tasks involving these data types such as AI or image/video/audio processing or NLP tasks. It provides most of the fundamental programming constructs to be called a general purpose programming language. It's syntax borrows from Python, C, Haskell and others (it does not build on Wren).

Dealing with file I/O in most languages understandably requires a few lines of code. Each language has its own syntax for this based on its standard library. Flow tries to abstract it all away through file data types and direct file referencing syntax. It focuses on the two key file operations: writing/creating and reading.

For Flow to be truly useful there is a lot of heavy lifting that the compiler back end needs to do which I won't elaborate on here. It'll also need a vast standard library for users to be able to do useful things and it'll have to at least match the efficiency of a language like Python for the syntactic improvements to be worth it.

Flow is made for a present/future (with cheaper models and compute) where amateur programmers will want to make a query that goes like: "hey this is my task, give it to this model to formulate it and then pass on the description to another model and query it 10 times and give me the best answer", or they need to perform a very complicated (and expensive) workflow of tasks and make individual models work together.

Implementation

Flow is statically typed and fairly strongly typed with the hopes that it'll make the file operations for the compiler faster.
The code blocks will show multiple constructs at once due to word limit.

Constructs

entry point

  • The entry point for a flow program is the main function like in C.
  • Whether Void (same as void in C) is the only output main can have and how the command line arguments are managed is left arbitrary by the grammar (which just sees main as another function definition).
  • The global space only contains function definitions; everything lives inside functions.
main :: Void {
    print() <- my_func() <- 5 <- 6;
}

my_func :: Int res <- Int a <- Int b {
    // insert code...
    // function definitions are allowed to be empty
}

function definition | function call | chaining

  • In function calls, "<-", the flow operator, is used to feed arguments.
  • For stylistic and logical consistency "<-" is also used in the function signature (kind of like Haskell), where the first type-identifier pair in the signature "chain" is for what is returned and the rest for the parameters.
  • "return" is used to exit the function and the value of the return variable (defined in the signature) at return time is the value returned by the function.
  • "<-" enables function composition that is hopefully more visual. A chain is always read and processed right to left (right associative) (note that brackets are calculated first) which leaves room for no ambiguity. Each function can only output one value so it is easy for the compiler to match that as a single argument to another function.
main :: Void {
    print() <- hypotenuse() <- 5 <- 6;
}

hypotenuse :: Int res <- Int a <- Int b {
    res := -1;
    if (!a || !b) {return;} // early return res
    res := (a**2 + b**2)**0.5;
    // res is returned implicitly here
}

recursive_processor :: Image res <- List images <- Int n {
    res := images;
    if (n == 5) {return;}
    List processed := [regenerate() <- (edit() <- crop() <- "550px" <- filter() <- choose_half() <- images) <- "dalle"]; // no ambiguity here
    res := recursive_processor() <- processed <- n+1;
}

async-await | default arguments | named arguments

  • Concurrency support provided by C#-style async-await (not like javascript).
  • Support for this is crucial since users of Flow will likely perform computationally expensive tasks!
  • A useful pattern in Flow would be to define an async function that writes to a file and this function is called without awaiting.
  • Support for python-style default and named arguments (uses "=" symbol).
main :: Void { 
    async connect_to_network :: String success <- String host <- String ip {
        success := await fetch() <- host <- ip <- "get";
    }
    String host := "ucl.ac.uk";
    String ip := "1.02.193.07";
    // note how connecting to network can be done asynchronously by not awaiting
    connect_to_network() <- host <- ip;

    other_work();
}
// default args
main :: Void {
    my_func :: Int res <- Int a = 10 <- Int b = 15 {
        res := b % a ;
    }
}
// named args
main :: Void {
    print() <- "hey" <- sep = " ";
}

basic types | single line comments

  • Support for escape sequences like \" or \n in strings. Having escape sequences as a separate token within the string also makes it easier for the back end to appropriately deal with them.
  • Variables are null by default if declared but not initiaized. Most operations performed on null would throw an error. Falsy/truthy behaviour is same as python.
  • Identifiers are forced to be snake case (highly opinionated personal choice).
main :: Void {
    // comments like javascript
    Int n; // n is null
    Int a := -5;
    Float b:= -7.7;
    Int c := 0xBABe; // hex
    Int d := 0b010100; // binary
    Bool e := true;
    String s:= "hello world";
    Float f:= null; // all types are allowed to be null
    // super important to allow " in strings for NLP tasks.
    String s:= "Sam shouted, \"Ichigo is my favourite game\".";
    String s2 := "\n";
    String s3 := "\\"; // this is just \
}

file data types | type conversion | list appending | imports

  • Assigning a value to a file data type is Flow's way of "reading/loading in" a file.
  • The @ symbol followed by a string path to the file is the "literal" that represents a file in Flow.
  • Flow will support many (but not all) type conversions/casting and will do so in whatever way makes the most logical sense (like Python). Some examples shown in code snippet.
  • Support for list appending (not in-place) using the [+] operator.
  • Support for imports destructuring. Note the use of @ symbol with .flow files for consistent syntax.
import {similarity, speech_to_text} @("stdlib/nlp_models.flow"); // use no braces for single imports

main :: Void {
    best_audition :: Void {
        Image dog := @("images/doggo.png"); // can do the same with video and audio
        Text data := @("shakespeare.txt");
        Dir auditions := @("/User/me/Downlaods/Theatre Auditions"); // space in filenames allowed
        List scores := [];
        for (Audio dialogue in List(auditions)) {
            scores := scores [+] similarity() <- data <- speech_to_text() <- dialogue; // no ambiguity here
        }
        print() <- max() <- scores;
    }
}

operators | assignment

  • all binary operators are left associative and their relative precedence is defined in the grammar to avoid ambiguity.
main :: Void {
    check_membership :: Bool is_in <- String ele <- List arr
    {
        is_in := (ele in arr);
    }
    negative_addition :: Int res <- Int x <- Int y {
        res := 0 - x - y; // left associativity: processed and parsed as (0 - x) - y rather than 0 - (x - y)
    }
    say_hi :: Void <- Int y <- String s {
        print() <- "hi" + s * y; // precedence: processed and parsed as "hi" + (s*y) rather than ("hi" + s)*y
    }
    something :: Int res <- Int x {
        res := 10**x;
        res += x;
        res *= x;
        res -= x;
        if !(x == 0) { // unary op "!"
            res /= x;
        }
    }
    // other operators: <<, >>, &&, ||, %, >, <, !=
}

if elif else | multi line comments

  • Note that functions like print, len, input, have not been explicitly defined in the grammar. The set of library functions is left arbitrary.
main :: Void {
    String a := input() <- "Please enter a number";
    /* note: if elif else blocks 
    are not allowed to be empty
    */
    if (Int(a) == 0) {
        /* multiline functions work anywhere! */func1();
    }

    elif (Int(a) == 1) {
        func2();
    }
    else {
        func3();
    }
}

while loop | for loop | range | slicing

  • Full support for Python-like heterogeneous lists and list slicing with start:end:step syntax.
  • Support for python range() equivalent. Written as (1...5), it is a list so it can be sliced as shown.
  • Loop control flow can be manipulated using continue and break.
main :: Void {
    print_arr :: Void <- List arr {
        for (Int i in (1...5)[::-1]) {
            print() <- arr[i];
        }
    }

    get_chars :: String res <- Text data <- Int n {
        res := "";
        data := String(data);
        Int i := 0;
        while (n > 0) {
            if (data[i] == "\n") {
                break; //breaking out of loop
                }
            elif (String(data[i]) in (0...9)) {
                i += 1;
                continue; //continuing to next iteration
                }
            else (data[i] != "\n") {
                res += data[i]; // string indexing and slicing possible
                n-=1;
            }
            i += 1;
        }
    }
}

writing/creating a file

  • You can write to file data types using the same assignment operator.
  • For optimization reasons in one off write cases flow allows writing to a file that has not been initialized yet without specifying a variable name (this only applies to file data types of course).
main :: Void {
    Audio @("sounds/new_music.mp3") := make_music(); // no variable name for one off write
    Video existing_video :=  @("yt/videos.mov");
    existing_video := edit_video() <- existing_video; // update existing file
}

About

my programming language "flow" (frontend only) for compilers class assignment

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •