~ngp

Lang Scribbles

This file contains some rough sketches of what a state-machine-centric, functional language would look like. As part of the design, I aim for the syntactic simplicity of Python with the powerful type system of Rust.

Init

Comments, as is obvious by this point, are 2 forward slashes //

C-style inline/multiline comments may be used like so:

/*

This is a multi-line comment

*/

========

Primary syntax features: * New-line and space delimited, no pesky semicolons * Comments are CommonMark formatted markdown * Capital letters on symbol definitions generally mean "public" interface, use these with caution. See more info below

========

Core concepts: The primary unit of work is a Machine. Machines are a set of functions, properties, and states. By default, a Machine will have exactly 2 states: Uninitialized and Initialized. These states are only relevant To Machines that define no states of their own, and in practice the only one you will ever see is Initialized. States are a sub-construct within Machines. They define and scope the properties and functions a Machine can be in. Properties and functions that are defined within a State are only accesible when the machine is within that state. Names that collide with higher-order names will be shadowed when within a state. That is, if I define a Machine with a top-level property foo and within the state bar I also define a foo, functions defined within the bar state will read and mutate the foo that is scoped to that state, leaving the outer-level untouched. It's recommended to not do this. States can be nested. Sub-states may or may not be matched when branching on a Machine's state, so use them sparingly.

Protocols

Protocols are very similar to Rust Traits, Go Interfaces, and Python's typing.Protocol. Protocols define Machine stub that may or may not define default implementations. A Machine that implements a protocol must implement the exact same state and function definitions as the protocol defines. It cannot overspecify states, and functions must be defined in the same scope as the protocol defines. For this reason, it is likely most Protocol definitions will not include states as part of their interface. Protocols can be composed together, forming a Protocol set. Protocol members are merged together. Names that overlap are assumed to be the same if their types match. If types do not match up, it is an error and the program will not compile. Machines may only implement one Protocol. If multiple protocols are desired, create a Prococol set and implement that.

Functions

Functions are more limiting than in most other languages. They do not support bindings, meaning you cannot declare a variable within a function's body, only branch and call other functions. The return value of the last function call is the return value of the function. Get used to writing Lisp.

Branching

Most languages implement if statements. Not this one. The only branching supported is structural pattern matching with syntax ~~stolen~~ borrowed from Rust. The most common pattern matching structure is on a Machine's state. All branches must always be handled.

Transmutation

Functions that take in a single argument and return a single, concrete type can be chained automatically by the compiler. This is best illustrated with an example. Say you want to open a file and read it's contents. To do so, we take a string representation of a file path, turn it into a configuration Machine, and finally into a File. One possible way to do this is like this:

func str_to_conf = (s: str) -> FileConf {
    FileConf.new(path = s)
}

func file_from_config = (c: FileConf) -> File {
    File.new(fd=open_fd(c))
}

A string can now be coerced into a File by implicit chaining

f: File = "./some_file.bin" // effectively calls file_from_config(str_to_conf(...))

M = state machine, contains states, properties, methods

P = protocol, an interface. A Machine can implement a Protocol implicitly or explicitly. Implicit implementations can be used within a module or package (file or exported lib), but must explicitly


//      The simple machine implements the processor protocol explicitly
Machine Simple[Job]: PProcessor[J] = {

    //S = state, a discrete state a Machine can be in. Contains properties, methods, and substates
    State waiting = {
        // P = protocol. This is a handle to something that implements the queue protocol/inteface
        job_queue: Pqueue
    }
    // Any subset of "State" is a valid signifier
    St processing = {
        // States can have discrete properties that only exist when in that state
        // These are freed/lost when they state changes
        // 
        job: Pjob
    }
    // *any* subset of "State".
    S errored = {
        error: Perror
        Srecoverable = {
            failed_job: Pjob
            // F = Function
            // E = none, nil, empty
            Freset: (s) -> E := Swaiting = {} // empty body
        }
    }
}

Machine Job = {
    // Captial letter state symbol names indicate this state transition is public
    // and can be transmuted by function bodies outside the scope of this Machine.
    // That is, you can do something like Mmy_job.SComplete(id=123, outputs={name: "test"}) to directly mutate the 
    // state of the job.
    S Pending {
        inputs: (str, u64)
    }
    S Complete {
        id: u64
        outputs: { name: str }
    }
    S Errored {
        message: str
        error: Perror
    }
}

// Describes something that has a close function that transitions into state closed
// How we get something in an open state is up to the implementor
Protocol Closeable {
    State open {
        Fclose = () -> Perror? := Sclosed
    }
    State closed {}
}

// Sub-protocols create protocol sets. The Reader protocol is whatever is defined here
// UNIONED with the body of Closeable. Collisions are merged.
Protocol Reader: P Closeable {
    S open {
        Fread = (count: int = 1) -> Perror[[u8; 0..$count]]
    }
}

Mfile: Reader {
    path: string
    func New = (path: string) -> PError[self] := Sopen {
        self.path = path
        match Fmake_fd(self.path) {

        }
        Sopen(handle=Fmake_fd(self.path))
    }
    S open {
        handle: fd

        Fread = (count: usize = 1) -> [u8; $count] {

        }
    }
    Sclosed {
        Fopen
    }
}

// builtins // These are stubs implemented by the runtime // T = type definition. Usually an alias, but can be a concrete definition of a tuple or a struct Type fd = ptr Fmake_fd(args) -> Tfd {}

Thoughts? Leave a comment