An Overview of qdbp
(*
Here is a multiline comment
(* They can nest :) *)
*)
; And here is a single line comment
(* Variables, ints, and strings *)
three := 3
hello_world := "Hello, world!"
(* We can bundle data together using prototype objects
A prototype is a collection of labeled methods *)
prototype := {
Method1[3] (* A method named `Method1` that returns `3` *)
(* `Method2` takes an argument and returns it *)
Method2[arg1 | arg1]
(* All methods take in an implicit `this` argument that is always set to the value
of the prototype it is in. Method3's return is equivalent to `prototype`*)
Method3[this]
}
empty_prototype := {}
(* We can invoke methods within prototypes. Note the period at the end *)
three := prototype Method1.
(* Methods with arguments must have the argument names specified *)
four := prototype Method2 arg1: 4.
four := prototype Method3. Method2 arg1: 4.
(* qdbp has no concept of functions in the traditional sense. However,
we can use a prototype with a single label to emulate a function. By
convention, we use the label `!` *)
identity_fn := {![x| x]}
(* From now on, we will refer to any prototype with the label `!` as a function *)
(* We can call the function by invoking the `!` method *)
five := identity_fn! x: 5.
(* Methods within a prototype can be overloaded by parameter names *)
print_args := {
![arg1 | arg1 Print.]
![arg1 arg2 |
_ := arg1 Print.
arg2 Print.
]
}
(* qdbp has no sequence expression. To evaluate multiple expressions, we just assign
them successively to variables *)
_ := print_args! arg1: "Hello World".
_ := print_args! arg1: "Hello " arg2: "World".
(* qdbp has no type annotations. Instead, it infers types.
For example, if `arg1` to `print_args` doesn't have a `Print` method, the
program will refuse to compile. Therefore, `print_args! arg1: {}.` will fail. *)
_ := print_args! arg1: 3. (* `print_args` is generic *)
(* To be specific, variable names must start with a lowercase letter.
Method names within a prototype must start with an uppercase letter or a symbol.
We can use method names with symbols to emulate operators. For example, int
objects have a `+` method with a single argument, `that` *)
five := 3 + that: 2.
(* Here is some syntax sugar that makes common prototype patterns less verbose *)
(* To make "functions" easier to define, this: *)
my_fn := { ![x| x] }
(* is equivalent to this: *)
my_fn := {x | x}
(* In other words, to make an object with a single method `!`, we can omit the
`[`, `]`, and `!` *)
(* Also, if we omit the name of the first argument, it is automatically assumed
to be `that` *)
five := 3 + that: 2. (* is equivalent to *) five := 3 + 2.
(* Periods are optional when invocation is surrounded by parentheses *)
three := identity_fn! x: 3. (* is the same as *) three := (identity_fn! x: 3)
(* All values are immutable. However, you can make a copy of a prototype with
changes *)
small_circle := {
Radius[3]
Diameter[(this Radius) * 2.]
}
(* Same as `small_circle` but `Radius` now returns 6 *)
big_circle := { small_circle
(* `Radius` needs to have the same type as the original `Radius`*)
Radius[6]
}
(*
qdbp uses a memory management strategy known as
Perceus Reference Counting that guarantees in-place
updates under the hood for many of these copy-and-modify
operations.
*)
twelve := big_circle Diameter.
(* Unfortunately we don't have full support for doubles yet, but stay tuned! *)
pi := 3
(* Prototypes can also be extended with new methods *)
big_circle := { big_circle
Area[(this Radius) * (this Radius). * pi.]
}
one_hundred_eight := big_circle Area.
(* Most of the time, the compiler can disambiguate between whether to extend
or replace. The one exception is when the original's prototype's field list
is dependent on a parameter. For example: *)
fn := {x |
{ x
Method[that | that]
}
}
(* The compiler can't disambiguate whether or not to replace the `Method` field in
`x` or to extend `x`. In situations like this, the compiler will always expect
that the original prototype contains the field and so will replace it. *)
(* We also can tag an object *)
object := {}
tagged_object := #TagName object
(* In qdbp, everything is right associative *)
tagged_object := #Tag 1 + 2. (* parsed as (#Tag (1 + 2.)) *)
(* An example application of tagged objects is to represent booleans *)
true := #True{}
false := #False{}
(* We can pattern match on tagged objects. For example, this is a function that
negates a bool. Like method invocation, notice the `.` at the end *)
negate := {that |
that
True? [#False{}]
False? [#True{}].
}
(* The type system will ensure that the pattern matching is exhaustive. For example,
if we don't handle the `#False` case for a value that could be `#False`, the
program won't compile *)
(* Another application of tagged objects is error types. Here is a safe
division by 0 function *)
safe_divide := {a b|
b = 0.
True? [#Error{}]
False? [#Ok a / b.].
}
two := safe_divide! a: 4 b: 2.
_ :=
two
Ok? [that|
that Print.
]
Error? [
"Error" Print.
].
(* Like any language, we want to be able to split our programs across multiple
files. Each qdbp program is a single expression that returns a value.
`@filename`'s result is the result of evaluating `filename.qdbp`. Filenames
can only have letters, `/`, `_`, and `.` *)
empty_list := @list
intlist := empty_list Append 3. Append 2.
(* qdbp does not support cyclic imports.
For example, this file cannot import itself *)
(* We can also call functions in the target language. See `samples/int.qdbp` for
an example *)
(* The return type of the function must be encoded in the function name.
It can either end with `_int`, `_string`, or `_bool` *)
(* To end a program abruptly, the special keyword ABORT will kill it *)
get_val := {that |
that
Ok? [that| that]
Error? [ABORT.].
}
(* If you were to import this file from another file, the result would be this
string *)
" This concludes the language reference and tutorial "