Copyright 2023 Brian Davis - CC-BY-NC-SA
Syntax Continued...
Comments
I see no reason to deviate from the C-standard comments, // and /**.
Literals
Literal | Type | Value |
---|---|---|
42 | Int | |
0b101 | Int | 5 |
0o10 | Int | 8 |
0x10 | Int | 16 |
3.14159 | Float | |
true | Boolean | |
false | Boolean | |
"Hello" | String |
Dyn does not have a null, nil or None value. Using an unitialized variable is a compiler error. To unallocate a variable one drops it from the local scope.
Operators
Most operators should be familiar from any language in the C family. I will only cover the exceptions.
() {} [] . <- ^ << >> | & ~ / * - + > >= < <= != == =
^ is the exponent operator.
<- is the arrow call operator. Given a function f that takes an argument x you can call it with either f(x) or f <- x. It is used for formatting functional pipelines without having to match parenthesis.
I am considering using an arrow operator pointing right, ie x -> f. That would allow you to read functional pipelines left to right (similar to F# I think) but I'm not sure I want to go that way. Grammatically we need to choose one or the other, or at least disallow mixing them because something like f <- g -> x is grammatically ambigious.
| & ~ >> << are the bitwise or, and, not and shift operators.
The most important operators are the block operators: {}. A list of expressions which are evaluated and result in a local scope object. Blocks are an expression and can be used anywhere an expression is called for.
Keywords
In the spirit of minimalism there are few keywords to remember.
and array boolean byref byte byval co const drop enum extend fixed float fun if/else int let not or return string struct typed unique valid where/is
and, or, and not or the boolean operators.
let is used for declaring variables. Variables must be declared. They can be initialized immediately or later but cannot be used before initialization.
let x: int = 42
Keep in mind that type inference makes type declarations on local variables largely unnecessary. Where type declarations are useful is in function signatures and in composite data types.
drop is the opposite of let, it removes the member from the local scope.
let a = if x==3? 4 else 5
let a
if x==3?
a = 4
else if x==4?
a = 5
else
a = 9
There is no difference between using if/else as an inline expression or stand alone.
let b = where x is 1? "a" 2? "b" else "c"
where x is
1? DoThing()
2? DoOtherThing()
else BreakThing()
My take on the switch/case (match/case in rust) statement is the where/is expression. I could have relied entirely on if/else for this functionality but I think the alternate form is valuable enough for the improved clarity when you want do something different depending on the values a single expression has. Rust kind of went bananas with all the things you can do with match/case. I love its completeness checking but I don't love all the syntax you need to learn for matching/destructuring. where/is simply takes an expression and checks for equality with one of a set values (also expressions). I want to provide some of the flexibility of rust's match/case but without inventing a whole new DSL for match expressions. If you wanted more flexibility you could do:
where true is
x < 5? DoThing()
x == 5? DoOtherThing()
x > 5? BreakThing()
I am considering adding a second form to make this slightly cleaner.
where a.y + z(5) as x
x < 5? DoThing()
x == 5? DoOtherThing()
x > 5? BreakThing()
fun vs co
All functions in Dyn are anonymous closures, perform tail call optimization including tail call mutual recursion. To name a function, simply assign it to a variable.
let f = fun(x:int) { return x + a }
Note: in the above a is an upvalue captured from the enclosing scope.
Note: For most of the code I've written so far I used the keyword def, because I'm used to python and because I liked that function blocks have deferred execution, but I think fun or fn is probably more obvious.
It is also possible to have a function that simply returns its local scope object.
let f = fun(x:int) { let a = x + 42 }
Will return an object with the members x and a.
To make a function into a continuation use co. While many languages have generators or coroutines, Dyn uses continuations which are a superset of both. Continuations preserve state between calls and can suspend and resume operation.
When return is used in a continuation its state is preserved for future use.
Note that there are no loop keywords such as while, for, or loop. All loop constructs can be built using recursion and while recursion is inefficient in many programming languages, the reason it is inefficient is because each function call extends the call stack. Allocating some amount of memory and deallocating it on each return. For loops with many iterations, this memory usuage really adds up. But by writing these recursive functions carefully, and having a compiler that performs tail call optimization (TCO), recursive algorithms can be exactly as efficient as regular loops. Given the design goal of minimalism I chose to implement TCO in the compiler and leave the loop constructs to the standard library.
A future article will explore the features of closures, continuations and recursion in more detail.
Primitive Types
Primitive types should be fairly obvious. Note that I have not yet settled on implementation details such 32 vs 64-bit types, etc. Generally type inference should mean you do not need to type local variables. Where types are most useful is in function signatures and object template definitions (ie classes). Even when sophisticated type inference could infer the types in a function signature, they are valuable documentation for the programmer.
boolean
byte
float
int
string
Composite Types
The only composite type is the local scope object, which allows both indexed, and named members of any types, and can be extended indefititely. However constraints can be added to these objects using a number of keywords to create object templates. Object templates could also be called user types, or classes.
To define an object template, preface a block with one or more constraints.
let MyTemplate = struct {
name: string
age: int
}
let MyObj: MyTemplate = [name="bob"; age=13]
Object templates also can be used anonymously.
let myArray: array fixed(10) typed(int) = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
Note the use of square brackets for composite type literals, where a declared type allows the compiler to coerce the literal into the correct data shape.
The three major data types are:
array constrains the object to only indexed (not named) members.
enum constrains the object to one of the member values. Modeled after rust enums/sum types.
struct creates the object with the provided named variables.
Further constraints:
const creates an immutable data structure.
byref by default, primitive types are passed by value, however if you wish to pass them by reference use byref.
byval by default, composite types are passed by reference, if you wish to pass them by value use byval.
extend use the given template as a starting point for this template. Similar to inheritance in python.
fixed constrains the length of an array or the members of a struct.
typed constrains all members to a single type.
unique constrains all values to be unique.
valid constrains all values with the given boolean expression. May be a function returning a boolean.