=== BAST language === ``` λ-CATS λ λ λ λ (ΘωΘ) (ΦωΦ) λ λ λ λ [ΘωΘ] [ΦωΦ] ``` File extensions: - file.bast - file.bst (.bas is already taken by BASIC) - file.☥ (if Mojo can have .🔥, I can have .☥ (also no 3-char Mojo extension?)) - file.𓋹 (and .𓋹 (they are different unicode values)) Goals: - Make a language with pretty syntax that is nice to look at and fun to hack in - Experiment with novel syntax ideas Features: - Compiled to wasm, specifically WASM-4 (at the start via [MoonBit](https://www.moonbitlang.com/)) - Linked lists, GC and dynamic typing - ALGOL-inspired syntax (infix notation, keywords...) - Mix of procedural and functional programming (not too far to any extreme) - Macros and macro-like functions - Compilation-oriented (meta)programming (or something like that) - Multiple ways to write the same thing Example: ``` module $main ;; function 'foo' that takes two arguments, 'bar' by value and 'baz' by reference func [foo bar:cpy baz]:[ data acc ;; local variables prefix:"in jim:" ;; local variables with default value ]:[ data := <-io ;; read to data fron STDIN for [in bar => 42]:+> ;; for loop with internal variable for [jim baz <= 37]:+> if [in < jim]:[ "{prefix} {in} {jim}\n" -> io ]:[ acc:+ in - jim ;; acc += in - jim in C-style syntax break [for 1] ;; break blocks can be specified by type and distance from ] ;; caller, this breaks inner most for loop [num data] + acc ;; convert data to number and retturn result of addition ] ``` Explanation: - ';' Marks comments. - '[]' Used to determine blocks, call functions in s-expression style and group text for macros. - ':' Is the bind delimiter and is used to bind tokens together on syntactical level. It tells the compiler that these tokens mean one thing. It can be used to give constructs multiple blocks, add operators to variables, modify operators, and much more! They are insensitive to whitespace. - '+>' This is a special kind of block, that spans to the end of it's parent block. Behaves exactly the same as if enclosed in normal block, but it makes syntax cleaner without going the indent-based-block route. - '->', '<-' These are the arrow operators, which are context sensitive. They do different things based on what variable types are on which side. - 'io' is a 'channel-like' object, that pretends it is a channel, but it actually just reads and writes to STDIN/STDOUT - Last value of a block is returned More examples: Variables: Outside of special cases (for loops...), variables must be declared in either the function of loop '' block, or with the 'var' construct. They can be assigned to by binding them with '=' (or other bindings, more on that later). Variables are case insensitive and usually use cebab-case. Snake_case does not work, due to underscore being used for attribute access. Variables can contain any alphanumerical character, '-', '?', '!' and any fancy unicode characters, but they can not start with a number. Variables cannot be any reserved name, and must be at least two characters long. Single characters are reserved for binding and other operations. ``` var [foo] var [bar : "Hello World"] var [foo-bar:(9)] foo := 42.0 var [ bax: 6 bach:9 faux:(bax+bach) ] ``` Constants: They can be declared similarly to variables, they must have a value and it must be a literal. ``` const foo:4 const [ bar:37 ] ``` Operators: Comparison operators can be chained like in Python. Each infix operator can be turned to a function by prefixing it with 'f' ``` ;; infix ; arythmetic foo + bar ;; addition foo - bar ;; substraction foo * bar ;; multiplication foo / bar ;; division foo % bar ;; modulo foo ^ bar ;; exponent foo // bar ;; remainless division foo ~ bar ;; concatenation ; comparison foo = bar ;; equals foo != bar ;; not equals foo <> bar ;; not equals foo < bar ;; less than foo <= bar ;; less or equal than foo > bar ;; greater than foo >= bar ;; greater or equal than foo == bar ;; deep comparison foo <=> bar ;; identity check ; locical foo && bar ;; logical and foo and bar ;; logical and foo || bar ;; logical or foo or bar ;; logical or foo ^^ bar ;; logical xor foo xor bar ;; logical xor ;; special foo << bar ;; rotate array/list left by bar foo >> bar ;; rotate array/list rigth by bar foo <- bar ;; context sensitive foo -> bar ;; context sensitive ;; prefix -foo ;; sign swap +foo ;; keep the current sign (useful!) !foo ;; logical not not foo ;; logical not `foo ;; deep copy ;; functions [f+ 4 6] ``` Operators and binding: Arithmetic operators can be either bound to variables, or they can have modifiers bound to them. When bound to variables, they act as a prefix operator that applies the variable value to it's argument with the side effect of storing the result to it's variable. (basically +=, -=, etc. in C-style) Beware; however, as not all only arithmetic operators can be bound to variables. Symbols of other operators might be used in variable binding, but they have nothing to do with the corresponding operators. Operators can also be prebound to variables, in which case they act the same as if bound, but return the old value instead. Operators can also be bound to variables from both sides, in which case the left one is evaluated first and then the second one is evaluated with the is evaluated with the new value. ``` ;; operator to variable foo:+ bar foo:- bar foo:% bar foo:~ bar ... ;; modifier to operator foo +:<:69 bar ;; don't go over 69 foo -:>69 bar ;; don't go under 69 foo +:<:37:420:> bar ;; keep in inclusive interval <37;420> foo +:69 bar ;; '|' can be ommited in literals foo *:<:baz bar ;; don't go over value of 'baz' foo *:%:baz bar ;; equivalent to '(foo * bar) % baz' foo +:%:<:37:420:> bar ;; loop within inclusive interval <37;420> foo ~:<:6 bar ;; don't go over 6 elements foo ~:%:6 bar ;; concatenate and shift left if over 6 elements ;; prebinding +:foo 1 + 2 ;; returns 'foo + 2', but foo is set to 'foo + 1' ;; combination +:foo:- 1 2 ;; equivalent to 'do [foo :+ 1 foo :+ 2]' ``` Special variable bindings: Outside of binding operators to variables, one can also bind other operations to variables. ``` foo:= bar ;; assigns copy of 'bar' to 'foo' foo:++ ;; adds one to foo foo:inc ;; adds one fo foo foo:-- ;; substracts one from foo foo:dec ;; substracts one from foo ``` Strings: Strings are denoted by '"'. Escape sequences can be triggered with '\' and interpolate by enclosing an expression in curly braces. Strings can span multiple lines. When a constant number is prepended to a string, up to that number of spaces is removed from each line in the string. This is useful for having multi-line strings work nicely with indentation. Prepending a 't' before a string causes it to become trimmed. ``` foo := "Hello, World!" bar := "<<{foo}>>" ;; -> "<>" baz := 2:t:" Errors have occurred. We won't tell you where or why. Lazy programmers. - NetPositive " ;; -> "Errors have occurred.\nWe won't tell you where or why.\nLazy programmers.\n - NetPositive" ``` Conditionals: There are three conditionals in BAST: 'if', 'unless', and 'cond'. 'if' consists of 2-3 blocks. First one marks the condition, second one what happens, if the condition is true and optional true what happens if it's false. 'unless' acts just like 'if', but it adds extra negation to the condition. 'cond' consists of a set of condition-block pairs. It goes through them in order until either one condition is not true, or until they run out. Remember that in BAST, every block returns a value. If no block gets executed, 'nil' is returned. 'cond' can also be written as only 'con'. There is also a 'case' statement, which acts similarly to 'cond', but it first takes a value and instead of conditions, it takes a list of values. If at least one of the values listed is equal ('=') to the given value, the branch is executed. ``` foo := if [foo > 7]:[bar]:[baz] unless [foo = bar]:[ [baz foo] ] ;; not sure about the styling of 'cond' yet, might change later cond [foo > bar]:[ ... ]:[bar > baz]:[ ... ]:[true]:[ ... ] con [foo > bar]:[ ... ]:[bar > baz]:[ ... ]:[true]:[ ... ] ``` Lists (linked): Linked lists are one of the main data structures in BAST. They consist of the usual cons cells, which can be created either by the 'cons' function, or with '\'. '\' is not an operator, as it does not obey normal precedence rules, so that they can be chained nicely to create lists. A proper list ends with 'nil', which must be explicitly joined upon the list, but as that would be tedious, 'nil' can also be referred to as 'N'. Alternatively, one can use the 'list' construct. The functions 'car' and 'cdr' can be used to access the head and tail of a cons cell. As it can be tedious to combine multiple 'car'/'cdr' calls, combined functions can be composed by chaining 'a's and 'd's in the function name, such as 'caddr' (same as [car [cdr [cdr]]]). This can also be approached from the other side by swapping the 'c' and 'r', such as 'rddac' (still same as [car [cdr [cdr]]]) Lists are iterable and can be measured by the 'len' function. Arrays are iterable and can be measured by the 'len' function. They can be further operated by the 'push', 'pop', 'insert' and 'pick' functions. They also have destructive versions 'push!', 'pop!', 'insert!' and 'pick!'. ``` foo := 1\2\3\4\N bar := list [1 2 3 4] baz := list [ 1\2\n 3\4\N 5\6\n ] [cons [car [cdr foo]] [caadr baz] [rdac baz]] [len foo] ;; -> 4 [push! bar 5] ;; returns and sets bar to: 1\2\3\4\5\N [insert bar 2 2.5] ;; returns 1\2\2.5\3\4\5\N and keeps bar as is ``` Arrays (lists, but that was already taken by linked lists, soooo): Arrays, while not as nice as lists, are useful for efficient storage of a large amount of data and for interacting with the outer world. Arrays can be created with curly braces and elements/slices can be accessed with '.'. Elements in an array are indexed from 0, as to not confuse newcomers. For even more efficiency and interaction with outer world, arrays can also be typed to any wasm datatype. Arrays are iterable and can be measured by the 'len' function. They can be further operated by the 'push', 'pop', 'insert' and 'pick' functions. They also have destructive versions 'push!', 'pop!', 'insert!' and 'pick!'. ``` foo := { 1 2 "Hello, world!" false (lamb [a b]:[a + b]) } ;; an array bar := i32:{37 42 69 420} ;; typed array by binding it to type name foo.1 ;; -> 2 foo.1.3 ;; -> {2 "Hello, world!" false} foo.1.3.2 ;; -> {2 false} (step of 2) foo.3 := 7 ;; assignment [len foo] ;; -> 5 bar := { 1 2 } [push! bar 3] ;; both returns and sets bar to: { 1 2 3 } [pick! bar 2] ;; returns 2 and sets bar to: { 1 3 } [pop bar] ;; returns 3 and leaves bar as is ``` Maps: Maps are used to store key-value pairs in an efficient manner. Any atomic value can be a key and any value can be a value. Maps are created with the 'map' construct and values can be accessed by '#' Maps are iterable and keys/values can be obtained with the 'keys', 'values', 'keys-a' and 'values-a' functions. ``` foo := map [ "hello":"world" 3:7 42 ;; value is 'nil' ] foo#"hello" ;; -> "world" foo#69 := 420 ;; -> new key [keys foo] ;; -> "hello"\3\42\69\N (in any order) [values-a foo] ;; -> {"world" 7 nil 420} (in any order) ``` Loops: Loops consist of two main blocks: head, where the loop is specified, and body, where the looping happens. Exception is the 'loop' loop, which only has the body. Just like with functions, constant and variable blocks can be added before the body. loop types: - for [ <<-|->|<=|=>> ]:[] Declares variable and loops it over values from the tail of the arrow to the one on it's head. Both inclusive, if the arrow is thin, or first inclusive, second exclusive if the arrow is thick. - while/until []:[] Repeats while/until a condition is true. First checks, then performs. - do-while/do-until []:[] Repeats while/until a condition is true. First performs, then checks. - over [ ]:[] | [ ]:[] Iterates over an iterable (list, array, map), either by only index/key, or by also a value. - loop [] Loops forever. break [ ]: Break is used to exit from a loop. It expects two things in it's block. A type of loop to break from and it's distance from the break. For example 'break [while 1]' exist the inner while loop. 'break [while 2]' skips the most inner and breaks straight from the second most inner 'while' loop. If the distance is omitted, 1 is implied. continue [ ]: Similar to 'break', but instead of exiting from the loop, it just skips to the next iteration. ``` for [in 0 -> 10]:[] ;; 0-10 for [in 0 => 10]:[] ;; 0-9 for [in 10 <- 0]:[] ;; 0-10 for [in 10 <= 0]:[] ;; 1-10 for [in 10 -> 0]:[] ;; 10-0 for [in 10 => 0]:[ ;; 10-1 if [in = 7]:[continue [for]] io <- in ] while [foo > 10]:[] until [foo > 10]:[ bar baz:37 ]:[] do-while [foo > 10]:[] do-until [foo != bar]:[] over [index iterable]:[] over [index value iterable]:[] loop [ loop +> loop [ind:0]:+> ind:++ if [ind > 10]:[ break [loop 1] ] break [loop 2] ] ``` Blocks: 'do', 'blk', 'blck' and 'block' are all constructs, that take a single block and simply execute it. There are no returns, no breaks and other fancy stuff, just the last expression is returned. You can think of `do [...]` as `if [true]:[...]`. ``` do [ foo := block [ bar := 6 bar * 9 ] ] ``` Functions: forms: - func [ ]:[] - func [ ]:[]:[] - func [ ]:[]:[]:[] Lambdas have the same forms as functions, except they have no name. They can be defined with either 'lambda', 'lamb' or 'λ'. Empty first block can be omitted, but only in the first form. Functions are called in a s-expression syntax. The pipe operator can be also used to pass a value to function call after it. return [] construct can be used to exit a function early. While all expressions, except chosen constructs, must be in functions, due to the nature of wasm, there is no standard entry point. ``` func [foo bar ;; any value baz:cpy ;; pass by value bax:num? ;; value must pass a predicate function 'num?' bach:=:43 ;; default value (extra '=', since can be a variable name in closures) ]:[] ;; same as: var [ foo lamb [bar baz:cpy bax:num? bach:=:43]:[] ] func [foo x:num?]:[ if [foo > 10]:[ return [false] ] ... ] [foo 1 2 3] ;; same as: 3 | [foo 1 2] ;; same as: 3 | 2 | 1 | [foo] ``` Predicates: Predicates are special functions that take a single argument, test if something is true for it and return a boolean. They are usually named by what they test for followed by '?'. ``` [num? foo] ;; is foo a number [string? foo] ;; is foo a string [char? foo] ;; is foo a character (if characters included) [bool? foo] ;; is foo a boolean [func? foo] ;; is foo a function [list? foo] ;; is foo a list (will brobably include nil) [array? foo] ;; is foo an array [map? foo] ;; is foo a map [atom? foo] ;; is foo an atomic value (will probably also include 'nil', complain to LISP) [nil? foo] ;; is foo nil [cons? foo] ;; is foo a cons cell (lists must end with nil) (false for nil) [struct? foo] ;; is foo a structure ;; predicate-like functions can also end with '?' [member foo bar] ;; does bar contain a value equal to ('=') foo [contains foo bar] ;; does bar contain foo ('<=>') ``` Structures (worth keeping both maps and structs?): Structures are yet another way to group multiple named values together. Unlike maps, they one can not add new values to a structure. Structures are defined with the 'struct' keyword. Well, actually a function which creates said structs is defined. In addition, corresponding predicate is also defined. First block of 'struct' is the same as with function. The body is a collection of variables, which exist within the struct. Variable name can be bound with initial value. Additionally, just like with function arguments, a predicate can be bound to a variable by placing it before the value. One can not provide only a predicate, so just explicitly bound nil. Variables in the structure can be accessed with underscore like so: var_value. If the value is a function of at least one argument, it can be transformed to a 'method' with by switching the struct/attribure order replacing the underscore with '@' like so: method@var. This returns the function with it's first argument prefilled with the structure. It both puts the structure to the argument position and follows the e-mail convention. Nice! ``` struct [foo xx]:[ bar baz:num?:37 ;; must be a number bax:xx bach: lamb [self foo]:[self_baz + foo] ] my-foo := [foo 42] my-foo_bar := my-foo ;; recursion! [foo? my-foo] ;; -> true in [my-foo]:[ ;; like 'with' in pascal baz := 42.0 bax -> io ] [bach@my-foo 9] ;; same as: [my-foo_bach y 9] ;; can be passed as a function fn := bach@my-foo [fn 9] ``` Parenthesis: Parenthesis can be used to group a bunch of code together as a single value. This can be useful for ensuring correct order of operations and in binding. ``` foo := (2 * (7 + 4)) arr.(foo-bar) var [ baz:([car lst]) ] ``` Channels: Channels are any structures, that implements the '--push', '--pop' and '--available' methods. They can then be manipulated with the arrow operators and the 'push', 'pop', and 'avail' functions. Simple FIFO and FILO stacks can be created with constructs of the same name. Channels are iterable. The index starts at 0 and they continue until '--available' returns false. ``` foo := fifo [ 1 2 3 4 ] [pop foo] ;; -> 1 [push foo [pop foo] + [pop foo]] ;; foo = <4 5> [avail foo] ;; -> true ``` Modules: [[[WARNING: MODULES WILL LIKELY BE DONE DIFFERENTLY, BUT I'M KEEMING THEM HERE ANYWAYS]]] Each file starts with the 'module' keyword, followed by a name, which must contain the '$' symbol (usually at the beginning). Each module can export multiple variables by putting their names in the 'export' construct. Modules are special, because they are not variables, since they exist only at compile time. To import a model, use the 'import' construct. In it's body, put names of the modules to import. Variables exported by a module can be accessed by binding it's name to the module name. To import a variable from the module straight to the current namespace, bind it to the module name in import. To create and alias to the module name, also bind it to it (it must still contain '$') To import all variables from a module to the current namespace, bind a star to it. ``` ;; foo.bast module $foo export [ foo-x foo-y foo-z ] var [ foo-x:4 foo-y:2 foo-z:0 bar:"Hello, World!" ;; not exported ] ;; bar.bast modle $bar import [ $foo:$f:foo-x ] func [main]:[ ;; all of the following now works foo-x -> io $foo:foo-x -> io $foo:foo-y -> io $f:foo-z -> io ;; exported variables are still mutable $f:foo-z := 37 ] ;; baz.bast module $baz import [ $bar:* ] func [main]:[ ;; now all are in the namespace directly foo-x := foo-y + foo-x ] ``` To be designed: Macros: Macros will be a way to create a used-defined 'constructs' that will be interpreted at compile-time. Their definition will be somewhat similar to function definition, but there will be some way to specify what separate blocks and what to expect in them, including special symbols (such as the arrow in for loop). Scheme macros should be taken as an inspiration. The symbol ','is reserved for usage in macros. Pattern matching: Pattern matching, while not essential, would be nice addition to the language. Pattern matching, outside of it's own construct, should probably also be usable in function/structure argument section. It should also be able to handle all lists, arrays, maps and structures.