A language based around extensibility and freedom
Quest supports everything you'd expect from a programming language and more!
- Simple, but powerful keyword-less syntax.
- Fundamentally based on hashmaps, not classes.
- Identifiers are first-class objects, just like everything else.
- Attributes and methods can be added to anything (including primitives!).
- Everything is fair game, including methods defined on primitives.
Quest is a "non-typed" language that is designed to allow for efficient code reuse. Similar to dynamically-typed languages in that types aren't relevant, Quest takes this a step further: There are no types (just key-value pairs).
See the examples
folder for some examples of what Quest can do! Most of them expect that you've read at least
# Text can either be single or double quotes: they're identical (like python).
where = 'world';
print('Hello, ' + where + '!'); # => Hello, world!
Local variables are actually just string keys on the current object. The following is identical to the previous example:
# `:0` is 'the current scope'.
:0.'where' = 'world';
print('Hello, ' + :0.'where' + '!'); # => Hello, world!
In Quest, there are no named/anonymous functions—they're both simply Block
s, written as { ... }
:
# Arguments are passed via local variables `_0`, `_1`, `_2`, etc.
# The last statement in a block is implicitly returned.
print('4 squared is: ', { _0 ** 2 }(4)); # => 4 squared is: 16
# You can assign anonymous functions to variables too. The `->` syntax
# can be used to name parameters.
square = n -> { n ** 2 };
print('4 squared is: ', square(4)); # => 4 squared is: 16
# You can even just straight-up add them to builtin classes:
# The `_0` argument is the the object that this method was called on, akin to
# `self` or `this` in other languages.
Number.square = n -> { n ** 2 };
print('4 squared is: ', 4.square());
Maps are created by simply returning the result of an executed block of code:
traffic_lights = {
# A blank scope is created whenever a block is called. Again, `:0` is the
# same as `this` / `self` in other languages.
:0.'red' = 'stop';
:0.'green' = 'go';
:0.'yellow' = 'go, if you can';
:0 # Return the current scope
}();
print('Green means ', traffic_lights.'green'); # => Green means go
However, you rarely need to do this directly. The object
function makes this a lot easier:
traffic_lights = object() {
'red' = 'stop';
'green' = 'go';
'yellow' = 'go, if you can';
};
print('Green means ', traffic_lights.'green'); # => Green means go
Classes are actually just objects too: They're just a group of methods which are available for any object which makes the "class" its parent. There is no intrinsic concept of a "constructor" either, and is generally implemented by overloading the "call" (()
) function and returning the new scope.
Person = object() {
# Whenever something is called, the `()` attribute is run.
# "Constructors" are really just defining a `()` attribute that overwrites `__parents__` and
# returns `:0` as the last value.
'()' = (class, first, last) -> {
# You can have multiple parents (to allow for multiple inheritance and mixins),
# However, here we don't need to have multiple parents.
__parents__ = [class];
:0.becomes(class); # However, this is generally used instead of the previous line as it's simpler.
# The `first` and `last` variables are already defined in the current scope, so we don't
# need to assign them!
:0 # return the current scope, i.e. the new object
};
# Define the conversion to a text object
@text = self -> { self.first + " " + self.last };
};
();
person = Person("John", "Doe");
print(person); # => "John Doe"
Sticking to the theme of extensibility and freedom, there aren't traditional "keywords." Traditional control-flow keywords (such as if
, while
, and return
) are simply attributes defined on the Kernel
object (which most objects inherit from). And traditional "definition" keywords (such as class
and function-declaration keywords) aren't relevant.
factorial = n -> {
# The if function executes whichever branch is chosen
if(n <= 1, {
1
}, {
n * factorial(n - 1)
})
};
print("10! =", factorial(10)); # => 10! = 3628800
i = 0;
while ({ i < 5 }) {
i += 1;
print("i =", i);
};
# => i = 1
# => i = 2
# => i = 3
# => i = 4
# => i = 5
The return
function is a bit different than other languages. Because there is no concept of "functions vs blocks", you must return to a specific scope:
make_dinner = {
the_magic_word = prompt("what's the magic word? ");
if(the_magic_word != "please", {
print("You didn't say 'please'!");
# `:0` is the current stackframe, `:1` is the stackframe above this
# one in this case, that's the `make_dinner` stackframe. return `false`
# from that stackframe.
return(false, :1);
});
# Alternatively, you can use the shorthand of `false.return`, which returns
# only a single level up.
if(the_magic_word != "please", false.return);
# Or even
(the_magic_word == "please").else(false.return);
collect_ingredients();
prepare_stove();
cook_food();
set_table();
print("food's ready!");
true # return `true`
};
# the `if` function can also be used as a ternary operator.
print(if(make_dinner(), { "time to eat!" }, { "aww" }));
This also removes the need for continue
and break
keywords that so many other languages have:
i = 0;
while ({ i < 100 }) {
i += 1;
# Quest supports "truthy" values.
if (i % 2) {
# Return from the while loops's body's stackframe.
# This is analogous to `continue`.
return(:1);
};
print("i =", i);
if (i == 8) {
print("stopping.");
# Return from the while loop's stackframe.
# This is analogous to `break`.
return(:2);
};
};
print("done");
# => i = 2
# => i = 4
# => i = 6
# => i = 8
# => stopping
# => done
(TODO: there's probably more I could do here to explain this better...)
Because there's no separate concept of an "identifier" in Quest (as all identifiers are really Text
s), there's no true l- or r-value concept. Instead, they are implemented via attributes defined on Text: =
and ()
.
Unlike most languages, =
is actually an operator. Only Text
has it defined by default (but like any other operator, anything can overload it.):
x = 5; # call the `Text::=` function implicitly
y.'='(6); # call the `Text::=` function explicitly
print(:0.x, :0.y); # => 5 6
# now you can assign numbers.
# however, you can only access them via `:0.XXX`.
Number.'=' = Text::'=';
3 = 4;
print(:0.3) # => 4
# Obviously this isn't that helpful for numbers, but it's how destructoring lists work!
(Minor note: a.b = c
doesn't actually use the =
operator; it's syntactic sugar for the .=
operator—a.'.='(b,c)
—and is accessible on every object that inherits from Pristine
(which is everything, by default).)
Text
also has the ()
method defined, where it simply looks up its value in the current scope: (Bare variables, eg foo
, were added so 'foo'()
wouldn't be necessary.)
'x' = 5;
print(x, 'x'(), :0.'x'); # => 5 5 5
Most runtime languages support some form of instance variables that can be added to objects. However, Quest takes this a step further, and allows everything to have attributes added/removed from them, including primitives like numbers. (For those language-savvy folks, every Quest object is a singleton object.)
# define the `square` method on Numbers in general.
Number.square = self -> { self ** 2 };
twelve = 12;
print(twelve.square()); # => 144
# define the `cube` method on this instance of 12.
twelve.cube = self -> { self ** 3 };
print(twelve.cube); # => 1728
# no other `12` in the program has access to the `cube` method.
print(12.__has_attr__('cube')); # => false
See the examples
folder for more examples of what Quest can do!
There's also some stuff I've written up in the docs
that goes more into depth.
- Clone the repo
- If you haven't already, install Rust and cargo
- Run
$ cargo build
to create the project ./quest [-h] [-f file] [-e script] [-- [args to pass to the quest program]]
- Command-line arguments are passed in the
__args__
method in the base script object.
- Command-line arguments are passed in the
If all arguments are omitted a REPL instance will be launched.
I should probably add more discussion of Quest's features.
PROGRAM := <block-inner>
expr
:= <primary>
| UNARY_OP <expr>
| <expr> BINARY_OP <expr>
| <expr> <block> # Function call
;
primary := <block> | <literal>;
block := '(' <block-inner> ')' | '[' <block-inner> ']' | '{' <block-inner> '}';
block-inner := (<line>;)* <line>?;
line := (<expr>,)* <expr>?;
literal := <ident> | <number> | <string>
ident := ...;
number := ...;
string := ...;