aa

Cliff Click Language Hacking

To-the-metal performance: every abstraction used can be peeled away, yielding code that you would expect from a tightly written low-level program. This is a primary driver for me, the guy who's been writing compilers for high performance computing (including Java) all my life. Ease-of-programming is a close second, but only after you've declared that performance is not your main goal. Hence I support syntactic sugar on expensive operations (e.g. accidental auto-boxing in a hot loop can cost you 10x in speed) but only if the type-system cannot remove it and you've not declared that you are programming for speed.

Modern: Statically typed. Functional programming with full-strength type inference - types are everywhere optional. Minimal syntax. REPL (i.e. incremental static typing) and seperate compilation. Typed C-style macros: most syntax errors in dead code are typed.

My intent is a modern language that can be used where C or C++ is used: low-level high-performance work and a systems implementation language (e.g. OS's, device drivers, GC's), but bring in all the goodness of the last 20 years of language improvements.

The language is intended to "look like" C or Java, but with all types optional and all keywords removed. The whole no-keywords thing is an experiment I may back away from; a primary goal is to be readable - sometimes keywords feel like clutter and sometimes they are an code-anchor for your scanning eye.

I specifically intend to look at real-time constraints and the notion of Time in a language.

Part of modern coding is the use Garbage Collection and the structured use of malloc/free - so I intend to add Rust-style memory management.

Part of modern coding is the use of multiple cores, so I intend to explore a couple of different concurrency models. A notion of a true 'final' field, made with a final-store (as opposed to declared as a final type) - you can make cyclic final structures, and once final all future reads will see the final value. No odd exceptions for "early publishing" a pointer.

And of course, this is a Work-In-Progress!!!!

GRAMMAR

BNF Comment
prog = stmts END
stmts= [tstmt or stmt][; stmts]*[;]? multiple statments; final ';' is optional
tstmt= tvar = :type type variable assignment
stmt = [id[:type] [:]=]* ifex ids are (re-)assigned, and are available in later statements.
stmt = ^ifex Early function exit
ifex = expr [? stmt [: stmt]] trinary logic; the else-clause will default to 0
expr = term [binop term]* gather all the binops and sort by prec
term = id++ | id-- post-inc/dec operators
term = tfact post A term is a tfact and some more stuff...
post = empty A term can be just a plain 'tfact'
post = (tuple) post Application argument list
post = tfact post Application as adjacent value
post = .field post Field and tuple lookup
post = .field [:]= stmt Field (re)assignment. Plain '=' is a final assignment
post = .field++ OR .field-- Field reassignment.
tfact= fact[:type] Optionally typed fact
fact = id variable lookup
fact = num number
fact = "string" string
fact = (stmts) General statements parsed recursively
fact = tuple Tuple builder
fact = func Anonymous function declaration
fact = @{ [id[:type][=stmt],]* } Anonymous struct declaration; optional type, optional initial value, optional final comma
fact = {binop} Special syntactic form of binop; no spaces allowed; returns function constant
fact = {uniop} Special syntactic form of uniop; no spaces allowed; returns function constant
tuple= (stmts,[stmts,]) Tuple; final comma is optional
binop= +-*%&/<>!= [] ]= etc; primitive lookup; can determine infix binop at parse-time, also pipe but GFM screws up
uniop= -!~ [] etc; primitive lookup; can determine infix uniop at parse-time
func = { [id[:type]* ->]? stmts} Anonymous function declaration, if no args then the -> is optional
str = [.\%]* String contents; \t\n\r\% standard escapes
str = %[num]?[.num]?fact Percent escape embeds a 'fact' in a string; "name=%name\n"
type = tcon OR tvar OR tfun[?] OR tstruct[?] OR ttuple[?] // Types are a tcon or a tfun or a tstruct or a type variable. A trailing ? means 'nilable'
tcon = int, int[1,8,16,32,64], flt, flt[32,64], real, str[?] Primitive types
tfun = {[[type]* ->]? type } Function types mirror func decls
ttuple = ([[type],]* ) Tuple types are just a list of optional types; the count of commas dictates the length, zero commas is zero length. Tuples are always final.
tmod = = | := | == '=' is r/only, ':=' is r/w, '==' is final
tstruct = @{ [id [tmod [type?]],]*} Struct types are field names with optional access and optional types. Spaces not allowed
tvar = id Type variable lookup

SIMPLE EXAMPLES

Code Comment
1 1:int
Prefix unary operator ---
-1 -1:int application of unary minus to a positive 1
!1 0:int convert 'truthy' to false
Infix binary operator ---
1+2 3:int
1-2 -1:int
1<=2 1:int Truth === 1
1>=2 0:int False === 0
Binary operators have precedence ---
1+2*3 7:int standard precedence
1+2 * 3+4 *5 27:int
(1+2)*(3+4)*5 105:int parens overrides precedence
1// some comment
+2
3:int with bad comment
1 < 2 1:int true is 1, 1 is true
1 > 2 0:int false is 0, 0 is false
Float ---
1.2+3.4 4.6:flt
1+2.3 3.3:flt standard auto-widening of int to flt
1.0==1 1:int True; int widened to float
Simple strings ---
"Hello, world" "Hello, world":str
str(3.14) "3.14":str Overloaded str(:flt)
str(3) "3":str Overloaded str(:int)
str("abc") "abc":str Overloaded str(:str)
Variable lookup ---
math_pi 3.141592653589793:flt Should be math.pi but name spaces not implemented
primitive function lookup ---
+ "Syntax error; trailing junk" unadorned primitive not allowed
{+} {{+:{int int -> int}, +:{flt flt -> flt}} returns a union of '+' functions
{!} !:{int -> int1} function taking an int and returning a bool
Function application, traditional paren/comma args ---
{+}(1,2) 3:int
{-}(1,2) -1:int binary version
{-}(1) -1:int unary version
Errors; mismatch arg count ---
!() Call to unary function '!', but missing the one required argument
math_pi(1) A function is being called, but 3.141592653589793 is not a function type
{+}(1,2,3) Passing 3 arguments to +{flt64 flt64 -> flt64} which takes 2 arguments
Arguments separated by commas and are full statements ---
{+}(1, 2 * 3) 7:int
{+}(1 + 2 * 3, 4 * 5 + 6) 33:int
(1;2 ) 2:int just parens around two statements
(1;2;) 2:int final semicolon is optional
{+}(1;2 ,3) 5:int full statements in arguments
Syntax for variable assignment ---
x=1 1:int assignments have values
x:=1 1:int Same thing, not final
x=y=1 1:int stacked assignments ok
x=y= Missing ifex after assignment of 'y'
x=1+y Unknown ref 'y'
x=2; y=x+1; x*y 6:int
1+(x=2*3)+x*x 43:int Re-use ref immediately after def; parses as: x=(2*3); 1+x+x*x
x=(1+(x=2)+x) Cannot re-assign ref 'x'
x=(1+(x:=2)+x) 5:int RHS executes first, so parses as: x:=2; x=1+x+x
x++ 0:int Define new variable as 0, and return it before the addition
x:=0; x++; x 1:int Define new variable as 0, then add 1 to it, then return it
x++ + x-- 1:int Can be used in expressions
Conditionals ---
0 ? 2 : 3 3:int Zero is false
2 ? 2 : 3 2:int Any non-zero is true; "truthy"
math_rand(1)?(x=4):(x=3);x :int8 x defined on both arms, so available after but range bound
math_rand(1)?(x=2): 3 ;4 4:int x-defined on 1 side only, but not used thereafter
math_rand(1)?(y=2;x=y*y):(x=3);x :int8 x defined on both arms, so available after, while y is not
math_rand(1)?(x=2): 3 ;x 'x' not defined on false arm of trinary No partial-defs
math_rand(1)?(x=2): 3 ;y=x+2;y 'x' not defined on false arm of trinary More complicated partial-def
math_rand(1)?1 :int1 Can skip last arm, will default to 0
x:=0; math_rand(1) ? x++; x :int1 Side effects in the one arm
0 ? (x=2) : 3;x 'x' not defined on false arm of trinary
2 ? (x=2) : 3;x 2:int Off-side is constant-dead, so missing x-assign is ignored
2 ? (x=2) : y 2:int Off-side is constant-dead, so "Unknown ref 'y'" is ignored
x=1;2?(x=2):(x=3);x Cannot re-assign ref 'x' Re-assignment not allowed
x=1;2? 2 :(x=3);x 1:int Re-assign allowed & ignored in dead branch
math_rand(1)?1:int:2:int :int8 no ambiguity between conditionals and type annotations
math_rand(1)?1::2:int missing expr after ':'
math_rand(1)?1:"a" Cannot mix GC and non-GC types
math_rand(1)?(x:=4):(x:=3);x :int8 x mutably defined on both arms, so available after
math_rand(1)?(x:=4):(x:=3);x:=x+1 :int64 x mutably defined on both arms, so mutable after
x:=0; math_rand(1) ? (x:=4):3; x:=x+1 :int8 x partially updated, remains mutable after
x:=0; 1 ? (x =4):; x 4:int8 x final on 1 arm, dead on other arm, so final after
x:=0; math_rand(1) ? (x =4):3; x 'x' not final on false arm of trinary Must be fully final after trinary
Anonymous function definition ---
{x y -> x+y} Types as a 2-arg function { int int -> int } or { flt flt -> flt }
{5}() 5:int No args nor -> required; this is simply a no-arg function returning 5, being executed
Identity mimics having type-vars via inlining during typing ---
id {A->A} No type-vars yet
id(1) 1:int
id(3.14) 3.14:flt
id({+}) {{+:{int int -> int}, +:{flt flt -> flt}}
id({+})(id(1),id(math_pi)) 4.141592653589793:flt
Function execution and result typing ---
x=3; andx={y -> x & y}; andx(2) 2:int capture external variable
x=3; and2={x -> x & 2}; and2(x) 2:int shadow external variable
plus2={x -> x+2}; x Unknown ref 'x' Scope exit ends lifetime
fun={x -> } Missing function body
mul3={x -> y=3; x*y}; mul3(2) 6:int // multiple statements in func body
x=3; addx={y -> x+y}; addx(2) 5:int Overloaded + resolves to :int
x=3; mul2={x -> x*2}; mul2(2.1) 4.2:flt Overloaded {+}:flt resolves with I->F conversion
x=3; mul2={x -> x*2}; mul2(2.1)+mul2(x) 10.2:flt Overloaded mul2 specialized into int and flt variants
sq={x -> x*x}; sq 2.1 4.41:flt No () required for single args
Type annotations ---
-1:int -1:int
(1+2.3):flt 3.3:flt Any expression can have a type annotation
x:int = 1 1:int Variable assignment can also have one
x:flt = 1 1:int Casts for free to a float
x:flt32 = 123456789 123456789 is not a flt32 Failed to convert int64 to a flt32
1: Syntax error; trailing junk Missing type
-1:int1 -1 is not a int1 int1 is only {0,1}
"abc":int "abc" is not a int64
x=3; fun:{int -> int} = {x -> x*2}; fun(2.1)+fun(x) 2.1 is not a int64
x=3; fun:{real -> real} = {x -> x*2}; fun(2.1)+fun(x) 10.4:flt real covers both int and flt
fun:{real->flt32} = {x -> x}; fun(123 ) 123:int Casts for free to real and flt32
fun:{real->flt32} = {x -> x}; fun(123456789) 123456789 is not a flt32
{x:int -> x*2}(1) 2:int types on parmeters too
{x:str -> x}(1) 1 is not a str
Recursive and co-recursive functions ---
fact = { x -> x <= 1 ? x : x*fact(x-1) }; fact(3) 6:int fully evaluates at typing time
fib = { x -> x <= 1 ? 1 : fib(x-1)+fib(x-2) }; fib(4) :int does not collapse at typing time
is_even = { n -> n ? is_odd(n-1) : 1}; is_odd = {n -> n ? is_even(n-1) : 0}; is_even(4) 1:int
is_even = { n -> n ? is_odd(n-1) : 1}; is_odd = {n -> n ? is_even(n-1) : 0}; is_even(5) 0:int
Simple anonymous tuples ---
(1,\"abc\").0 1:int .n loads from the nth field; only parse-time constants are supported
(1,\"abc\").1 "abc"
Simple anonymous structures ---
@{x,y} @{x,y} Simple anon struct decl
a=@{x=1.2,y}; x Unknown ref 'x' Field name does not escape structure
a=@{x=1,x=2} Cannot define field '.x' twice
a=@{x=1.2,y,}; a.x 1.2:flt Standard "." field name lookups; trailing comma optional
(a=@{x,y}; a.) Missing field name after '.'
a=@{x,y}; a.x=1 Cannot re-assign field '.x' No reassignment yet
a=@{x=0,y=1}; b=@{x=2}; c=math_rand(1)?a:b; c.x :int8 Either 0 or 2; structs can be partially merged and used
a=@{x=0,y=1}; b=@{x=2}; c=math_rand(1)?a:b; c.y Unknown field '.y' Used fields must be fully available
dist={p->p.x*p.x+p.y*p.y}; dist(@{x=1}) Unknown field '.y' Field not available inside of function
dist={p->p.x*p.x+p.y*p.y}; dist(@{x=1,y=2}) 5:int Passing an anonymous struct OK
dist={p->p.x*p.x+p.y*p.y}; dist(@{x=1,y=2,z=3}) 5:int Extra fields OK
dist={p:@{x,y} -> p.x*p.x+p.y*p.y}; dist(@{x=1,y=2}) 5:int Structure type annotations on function args
a=@{x=(b=1.2)*b,y=b}; a.y 1.2:flt Temps allowed in struct def
a=@{x=(b=1.2)*b,y=x}; a.y 1.44:flt Ok to use early fields in later defs
a=@{x=(b=1.2)*b,y=b}; b Unknown ref 'b' Structure def has a lexical scope
dist={p->p//qqq
.//qqq
x*p.x+p.y*p.y}; dist(//qqqbr>`@{x//qqq`<br=1,y=2})
5:int Some rather horrible comments
Named type variables Named types are simple subtypes
gal=:flt gal{flt -> gal:flt} Returns a simple type constructor function
gal=:flt; {gal} gal{flt -> gal:flt} Operator syntax for the function
gal=:flt; 3==gal(2)+1 1:int Auto-cast-away gal to get a flt
gal=:flt; tank:gal = gal(2) 2:gal
gal=:flt; tank:gal = gal(2)+1 3.0 is not a gal:flt No-auto-cast into a gal
Point=:@{x,y}; dist={p:Point -> p.x*p.x+p.y*p.y}; dist(Point(1,2)) 5:int type variables can be used anywhere a type can, including function arguments
Point=:@{x,y}; dist={p -> p.x*p.x+p.y*p.y}; dist(Point(1,2)) 5:int this dist takes any argument with fields @{x,y}, Point included
Point=:@{x,y}; dist={p:Point -> p.x*p.x+p.y*p.y}; dist(@{x=1,y=2}) @{x:1,y:2} is not a Point:@{x,y} this dist only takes a Point argument
Nilable and not-nil modeled after Kotlin ---
x:str? = 0 nil question-type allows nil or not; zero digit is nil
x:str? = "abc" "abc":str question-type allows nil or not
x:str = 0 "nil is not a str"
math_rand(1)?0:"abc" "abc"? Nil-or-string "abc"
(math_rand(1)?0 : @{x=1}).x Struct might be nil when reading field '.x' Must be provable not-nil
p=math_rand(1)?0:@{x=1}; p ? p.x : 0 :int1 not-nil-ness after a nil-check, so field de-ref is OK
x:int = y:str? = z:flt = 0 0:int nil/0 freely recasts
"abc"==0 0:int Compare vs nil
"abc"!=0 1:int Compare vs nil
nil=0; "abc"!=nil 1:int Another name for 0/nil
a = math_rand(1) ? 0 : @{x=1}; b = math_rand(1) ? 0 : @{c=a}; b ? (b.c ? b.c.x : 0) : 0 int1 Nested nilable structs
Recursive types ---
A= :(A?, int); A((0,2)) A:(nil,2) Simple recursive tuple
A= :(A?, int); A(0,2) A:(nil,2) Same thing using explicit args
A= :(A?, int); A(A(0,2),3) A:(A:(nil,2),3) Simple recursive tuple
A= :@{n:A?, v:flt}; A(@{n=0,v=1.2}).v 1.2:flt Named recursive structure
A= :@{n:A?, v:flt}; A(0,1.2).v 1.2:flt Same thing using explicit args
A= :@{n:B?, v:int}; a = A(0,2); a.n nil Unknown type B is never assigned, so no type error
A= :@{n:B, v:int}; B= :@{n:A, v:flt} B(@{n:A:@{n:B, v:int},v:flt} -> B) Types A and B are mutually recursive
List=:@{next:List?,val} List Linked-list type
List(List(0,1.2),2.3) List:@{next:List:@{next:nil,val:1.2},val:2.3} Sample linked-list, with all types shown
map_sq={x -> x ? (map_sq(x.0),x.1*x.1) : 0}; map_sq((0,1.2)) (nil,1.44) Strongly typed map_sq with a simple tuple
map={tree fun -> tree ? @{l=map(tree.l,fun),r=map(tree.r,fun),v=fun(tree.v)} : 0} Map a function over a tree in postfix order
Final fields are made with a final store not a final declaration ---
x=1 1:int Final local variable
x:=1 1:int Non-final local variable
x:=0; a=x; x:=1; (a,x) (0,1) Reassign local variable
x=1; x:=2 Cannot re-assignal final val 'x'
math_rand(1)?(x:=4):(x:=3);x:=x+1 int x mutable on both arms, so mutable after
x:=0; 1?(x=4):; x 4 x final on 1 arm, dead on other arm
x:=0; math_rand(1) ? (x =4):3; x 'x' not final on false arm of trinary Must be marked final on both arms, or dead on one.
x=@{n:=1,v:=2} @{n:=1,v:=2 Mutable field declaration and initial writes
x=@{n =1,v:=2}; x.n=3 Cannot re-assign read-only field '.n' Field initialized as final/read-only, cannot be changed
ptr0=@{p:=0,v:=1}; ptr1=@{p=ptr0,v:=2}; ptr0.p=ptr1; ptr0.p.v+ptr1.p.v 3:int final pointer-cycle is ok
ptr2rw = @{f:=1}; ptr2final:@{f==} = ptr2rw; ptr2final *@{f:=1} is not a *{f==} Cannot cast-to-final, can only make finals with a store

LARGER EXAMPLES:

Build a list from tuples; first element is payload and second element is the rest of the list, with nil terminating the list:

  lst = (1,(2,(3,0)))

Find and return the first element of a list passing a predicate:

find = { list pred ->     // find is a 2-arg function
  !list ? ^0;             // if list is nil, return nil
  pred(list.0) ? ^list.0; // if the 0-element passes the predicate, return it
  find(list.1,pred);      // otherwise, recursively find on the rest of the list
}

Find the first odd element:

find(lst,{e -> e&1})

...which returns 1.

Here is a simple map call, mapping fun over the list elements:

map = { list fun ->                // map is a 2-arg function
  list                             // list is nil-checked
  ? (map(fun,list.1),fun(list.0))  // return a list (tuple) composed of apply map to the 1-element and fun to the 0-element
  : 0                              // return a nil
}

Double the list elements:

map(lst,{x -> x+x})

Returns (2,(4,(6,0))).

Both map and + calls are generic, so a list of strings work as well:

map( ("abc", ("def", 0)), {x -> x+x} )

Returns ("abcabc", ("defdef", 0)).

Done Stuff

Ideas, Desirables

lifetime:

concurrency:

types and name spaces and nils:

performance types:

`fun copyInt2Dbl( src:[]int32, dst:[src.len+0]d64 )...`

OR

`fun copyInt2Dbl( len:int32, src:[len]int32, dst:[len]d64 )...`

OR

`fun slide( len:int32, off:int32, src:[>=len]a, dst[>=len+off]a )...`

maps & parallel loops:

serial loops:

for( foo in foos )
  if( isAcceptable(foo) )
    break;
  else return DidNotFindItError()
    if( foos.empty() ) return foos_was_empty
    else
      for( foo in foos )
        if( isAcceptable(foo) )
          break;
      else return no_acceptable_in_foos()

misc:

Getting started

Download dependencies:

make lib

Build:

make

Run checks:

make check

Launch the REPL:

java -jar build/aa.jar