-
Notifications
You must be signed in to change notification settings - Fork 6
The Complete Brat
This document is in a very rough state. At the moment, it is just a scrambled bunch of information.
The intent of this document is to detail all there is to know about the Brat language. I suspect in the future this should be chopped up into individual pages and hyperlinked and spellchecked and notarized. With a fancy table of contents.
Brat programs are executed from top to bottom.
Brat is an extremely eager language, which can be dangerous. The only way to delay evaluation is to put code inside of a function.
The syntax of Brat is supposed to be simple. Brat is relatively lenient about white space and tries to allow it where it makes sense (but not always).
Brat does not have any keywords. Anything that looks like a keyword is just a function or possibly an object.
Comments in Brat start with a pound sign (#
). They can be one line, like this:
# This is a comment until the end of the line
Or they can be over multiple lines, using #*
and *#
like this:
#* This is a comment
that starts on one line
and ends somewhere else!
*#
Brat is not too picky about the whitespace inside comments, and comments may be nested.
If a multiline comment opens with #*
but never closes with *#
, it will stretch to the end of the file. This can be useful, sometimes!
Variables are just the names we call things. Just labels to help us understand this crazy world we live in. They confer no meaning upon the things to which they refer. But they are useful for keeping track of values which we need later or again and again.
Variables must start with a letter. After that, they may contain numbers and (nearly) any combination of the following symbols: _!?\-*+^&@~/\><$%
. Variables may end with whatever.
Variables spring into existence when needed. They do not need any special declarations or definitions. They are very modest.
["Does that mean I can't use underscore prefixes?" It does mean that, indeed.]
Everything in Brat is either an object or a function. This keeps life simple. At least for the programmer.
["Why aren't functions objects, too?" Mainly because it's hard and annoying and the benefit (if any) is too small in comparison.]
Functions in Brat are closures. This means they can access variables from their surrounding scope and retain that access for their entire lifetime.
Defining Functions
Functions look like this:
{ required_argument, default_argument = true, *variable_length_args |
# The hash, number sign, octothorp, whatever, marks a one-line comment like this
}
In the above example, there is a required argument, an argument with a default value, and a variable length argument which will suck up 0 or more arguments into an array. Should you use all three types, this is the required ordering. But you don't need to use all of them! Just what you need. If you don't need any, don't use any. The vertical bar |
separates the parameter list from the "body" of the function. You do not need to use such a separator if you have no parameters. A new line is also not required after the separator. Do as you wish.
Calling Functions
Functions can be called with or without parentheses.
Anyway. Let's say you assign a function to a variable like so:
my_function = { } # This is a totally empty function
Then you do this:
my_function
Now you have called my_function
. And it was completely unexciting.
Let's define an only very slightly more exciting function:
add_3_things = { x, y, z | x + y + z }
You might guess that this takes three things and adds them together. You might have been expecting a return
statement of some kind, but Brat doesn't use those. It just returns whatever the result of the last expression in the function is. In this case, whatever adding x + y + z results in.
OKAY. GOT IT.
Right, so now you can call your new method like this:
add_3_things(1, 2, 3)
And I'm pretty sure it will return the number 6
. (Pretty sure.)
That's cool and all, but I think I mentioned that you don't (always) need parentheses. This is a pretty simple case, so it will probably be safe to try that now:
add_3_things 1, 2, 3
And I'm pretty sure it will still return the number 6
. That's pretty cool, too. But we can go further. We don't really need to have commas in there, so let's do it like this:
add_3_things 1 2 3
Now we are getting somewhere. I mean, who needed those parentheses and commas anyway?
What is important to keep in mind is that Brat is a very eager language. It wants to do things right now. It is the little kid in the back seat asking if we are there yet. It wants to know why it has to wait until after dinner to have dessert. Brat doesn't want to wait for anything. It is a product of an instant gratification culture.
To illustrate the immediate danger of such an attitude, consider this example:
a = 1
b = 2
c = 3
add_3_things a b c
AUGH TRAINWRECK MEDIC HALP
Should you try to run this, you will see some error like: Tried to invoke non-method: b
WHAT
Well, numbers were easy. We know what numbers are. But, what about a
, b
, c
? Well, they are either objects or functions. But they look like functions to Brat (because Brat is the kid kicking the back of your seat in the otherwise comfortable [sometimes sticky] darkness of the movie theater).
So this is what Brat sees:
add_3_things(a(b(c)))
Then it tries to call b(c)
but finds out b
is not a function. And it all falls apart.
So remember Brat's eagerness. It's going to evaluate your function arguments right now right now. So, in this case, you should use commas, which keep everybody separated and civil.
HOWEVER, consider this:
add_3_things 1 2 add_3_things 3 4 5
Now we're just getting silly. This is the way it goes:
add_3_things(1, 2, add_3_things(3, 4, 5))
Producing 14
or something close to it. Just remember, a bare variable followed by anything is going to be a function call. And the "anything" will be the arguments. But if you are using literals (like numbers, hashes, arrays, functions) then you are good to go with the dropping of the commas.
Wait, what about all those weird argument types?
Brat supports required arguments, arguments with default values, and variable length arguments. They go like so:
- Required arguments - look like normal formal parameters
- Default values - variable name followed by
=
and a value of some kind - Variable length arguments - variable name preceded by
*
will put all further arguments into an array
Also, skip down to hashes for information on a cool way to use them as arguments.
Anything Else?
Yes, a confession. Please keep this between just the two of us (Will Smith). Functions in Brat are a little lenient on dealing with extra arguments. That is to say, if you don't provide enough arguments for a function, it will complain. But if it gets more than it deserves, it won't mind. Brat is greedy that way.
[You might wonder why. It's because Lua is this way and enforcing anything else leads to inconsistencies I would rather avoid.]
Anything Else?
Yes, one more thing. What if you want to put a function in a variable, and then pass that variable into a different function? Remember, just using the variable will execute the function, and that's not what we want to do.
For example:
dont_do_this = { f | } # Function that takes a single argument, does nothing
say_hi = { p "Hi!!" } # Function that prints 'hi'
dont_do_this say_hi # Prints 'hi', returns null
Hey! We said don't do that! (Try this code yourself to convince yourself of what's going on).
How do we get around this issue? With the "don't do that" operator, of course. It's looks like this: ->
. I'm sorry it is ugly. I am used to it now. I am pretty sure it's the only truly "weird" syntax we have.
Adjusting the example above, we get:
dont_do_this = { f | } # Function that takes a single argument, does nothing
say_hi = { p "Hi!!" } # Function that prints 'hi'
dont_do_this ->say_hi # Does nothing, returns null
Ah, better. Now we can pass functions around via variables without concern.
[Alternatively, ->
can be thought of as the "value of" operator. On any value, it will return the thing itself. In the case of functions, this means the functions themselves, not their return value.]
Wow, all that time on functions. Let's talk about something else.
Objects are data structures which can have methods. And methods are just functions which have been assigned to a particular object.
Object Creation
Objects are created by creating a new version of an existing object (usually the base object which is just called object
). This is typically done by called the new
function on an object:
my_object = object.new
Defining Methods
Sorry to mislead you, but, as mentioned above, there aren't really method definitions in Brat. Just functions assigned to what other languages might refer to as "slots" on the object.
This is how the assignment works:
my_object.some_method = { "I'm just some method" }
This is how invocation works:
my_object.some_method
BUT WAIT. What if I want to assign something other than a function?
Glad you asked. When assigning something other than a function to what otherwise looks like a method definition (i.e., that example above), the "something" you assign will get wrapped in a method. So, it's still objects and functions, but kind of behind the scenes and out of the way.
my_object.a_number = 1 # Because we are number one
My
When invoking a method on an object, there is an implicit variable called my
which available inside the function. This variable refers to the object which the method is being invoked on. It's like this
or self
in some other languages.
Also, the function is evaluated in the context of the object. This means any variable lookups will occur as if they were methods called on my
.
my_object = object.new
my_object.something = { my.something_else = 'hello' }
my_object.something_further = { something_else }
my_object.something # Sets my_object.something_else
my_object.something_further # Returns "hello"
Be careful with my
, though. It may not always be obvious what value it contains.
Object Hierarchies or Whatever
In Brat, there is a fairly simple relationship between objects. When you call new
on an object, it will (IN GENERAL) create a new object with the existing object as its parent. Then, when looking up method access, the parent will be checked for methods if a matching method name is not found on the current object. Okay, I think I need to clarify that.
my_first_object = object.new
my_first_object.awesome_method = { "Awesome!" }
my_second_object = my_first_object.new
my_second_object.awesome_method # This would return "Awesome!" because it is
Here you can see that my_second_object
"inherited" awesome_method
from its parent. Now, a question may arise in your mind. "What happens if a parent object changes the method?" Well, then the child would be calling the new version.
Squishing
Brat provides another mechanism for "inheriting" methods. It's a little odd, but it can be useful. Sometimes. It's the squish
method. This method will take all the methods from an object and assign them (with the same names) to a different object. In this case, the "parent" object can change its methods without the child being affected whatsoever.
Example:
tomato = object.new
tomato.gross? = false # Actually, this is wrong, because tomatoes are gross
hamburger = object.new
hamburger.squish tomato
hamburger.gross? # This returns "false" which is clearly not true
tomato.gross? = true # Ah, the world has been set right
hamburger.gross? # This still returns "false"!! OH NOES APOCALYPSE
[Note to self: Create more coherent example.]
Brat supports a few different kinds of literal values. In particular, it has numbers, strings, symbols, functions, arrays, hash tables, regular expressions, and...never mind, that's it.
Right now, Brat is a little limited in this area. Basically, numbers look like numbers but nothing fancy like scientific notation or hex or binary.
Strings live between either double or single quotes. Something like
"I'm a string!!!1"
You can also put strings inside strings. Quantum physicists love this.1 There is some syntax for this that might or might not look familiar:
"I'm a #{ "string" + "!!!!1" } "
This #{}
thing actually evaluates the code inside the string (just once) and then tries to replace itself with the value it returns. So, it could be anything.
There is also a shorter way to create strings: by prefixing them with a colon:
:this_is_a_symbol
Symbols are like strings but they are immutable and only created once (per file 😞), so they are handy when you need to use them over and over again.
Functions are described above. They are included here because functions are values (just not objects, because they are functions instead). Therefore, they can be used like any other value. Used as arguments. Stored in arrays (below). Shared with friends, even.
Arrays are dynamically sized. Arrays have square brackets on either side. Like this:
[ 1, 2, 3, 4 ]
Arrays needn't be homogeneous. Nor is anyone going to force them to be heterogeneous. Also, like arguments to functions, commas are "sometimes food". Er. I mean, they are sometimes optional:
[ 1 2 3 4 5]
[:a 1 :b 2]
Just watch out for variables. They are greeeedy.
Array Access
Accessing arrays will probably look familiar. It uses square brackets again. Array indices start at 0
, just as Dijkstra intended.
my_array = [:a :b :c]
my_array[0] # This returns "a"
You can also use negative indices, with -1
being the last value in the array, -2
being second-to-last, and so on.
my_arrays[-1] # Returns "c"
Slicing and dicing of arrays is also possible. Note that the result will be from the starting index to (and including) the ending index:
my_array[1, 2] # Returns "[:b, :c]"
If the end index is less than the starting index, then the indices are swapped. But the result is still in the original order:
my_array[-2, 0] # Returns "[:a, :b]"
Bounds Checking
Brat is very liberal with what it accepts during array accesses. A single index beyond the length of the array will return null
. If slicing an array, then the indices will simply be truncated in a way that probably makes sense.
Index Assignment
Values can be assigned to any integer index. A negative index will wrap around as above. It looks like this:
my_array = [] # Empty array
my_array[10] = "hello" # my_array is now: [null, null, null, null, null, null, null, null, null, null, "hello"]
Hash tables look very similar to arrays. This is because we only have parentheses, curly braces, and square braces. Even vertical bars are already being used. Therefore, unfortunately, hash tables also use square brackets.
Hash tables or associative arrays or dictionaries or whatever you would like to call them, contain pairs of values. The first is the index or key, and the second is a value associated with that key. In Brat, the syntax goes key: value
:
[ 1 : "hello", 2 : "goodbye" ]
Like some other unnamed languages, there is a small shortcut when using string keys:
[ a: 1, b: 1, c: 3 ]
Again, commas are often optional:
[ a: 1 b: 2 c: 3 ]
Hash access is just like array access:
my_hash = [ a: 1 b: 2 c: 3 ]
my_hash[:a] #Returns "1"
my_hash[:awesome] #Returns "null"
Empty Hashes
Gee, hashes look a lot like arrays, then. I bet you want to know what an empty hash literal looks like. Well, it looks like this:
[:]
Underwhelming, huh?
Hash Table Arguments
Hash tables have a nifty syntax for using them in function calls. Basically, you can do this:
f = { test | p test[:first] }
f first: 1 second: 2 # Prints 1
You can intermingle these kind of "keyword" arguments anywhere in your argument list, but they will be gathered into a single hash table and assigned to the last formal parameter.
Causing all kinds of parsing headaches, regular expressions are denoted by backslashes / /
. They are pretty basic at the moment. They are implemented using Oniguruma, which is the same regular expression engine that Ruby used to use. That means that pretty much any documentation for Ruby regular expressions (in terms of syntax) will apply to Brat, too. Probably.
[Note to whomever: should expand this section.]
In Brat, as in some other languages, there are only two values that are considered false: false
and null
. All other things are considered true, to the best of our knowledge.
null
is an annoying thing that bothers everyone. The inventor of null
regrets it. In Brat, it is most often used to mean "nothing". For example, an array index with nothing in it will return null
. A hash key with no value assigned to it will return null
. And so on.
But, in Brat, null
is just an object. Actually, it is an object and a method, but that hardly matters. While you can reassign null
to a different value, I don't recommend it. It sounds like a terrible idea.
false
is pretty much like null
except it is called false
. It's used when things are untrue.
true
is like false
and null
except it is true and also called true
. It's used when things are true.
Binary operations are functions involving a left hand side value and a right hand side value. Brat uses "infix" notation, which means the operator sits in the middle of everything like the birthday boy (or girl) on their birthday.
Quite a few familiar mathematical operations are binary operations, like addition and multiplication and such.
In Brat, these operations are really just method calls. In other words, the familiar
3 + 4
is actually interpreted as
3.+(4)
This may not surprise you, because you are wise and know of other languages that operate in a similar fashion. However, Brat is a little different. Binary operators are really and truly just syntactic sugar. All of them (except the assignment operator, which isn't). This may cause surprises for you, because some people expect some logical operators (such as "or" and "and") to short-circuit. That is, once enough is known to determine the truthiness of a statement, evaluation halts.
For example, take this common sight (WARNING: THIS IS NOT BRAT CODE):
something_something || abort("Failure, all is in ruins.")
The idea here is that if something_something
is true
, then the program goes on its merry way. If, however, it is false
, then execution is halted.
This does not happen in Brat. There are two reasons. The first reason is that the code above would be interpreted like this:
something_something.||(abort("Failure, all is in ruins."))
With abort()
as a method argument, it surely needs to be evaluated before being passed into the method. And it is.
The second reason is that all operators are methods, and all methods may be redefined. Therefore, all operators are treated equally. Brat does not know what ||
means to your object and does not presume to know.
A binary operator may be any mix of symbols (!?\-*+^@~/\><$_%=|&
...and maybe others). When the operator is placed in between two expressions, it is called as a method on the left hand side expression with the right hand side as an argument:
knight = new
knight.=||----> = { rhs | p "Skewered #{rhs} with a sword!" }
knight =||----> "Bob"
Binary operators may be chained in any manner one sees fitting. After all, it is perfectly natural to do
1 + 2 - 3 * 5
And now is where I mentioned that I may not have been entirely honest about no operators being given special treatment. In order to maintain some sanity in this hurricane of illogic, basic operator precedence is defined for common operators. This includes math operators. "RAGE" you rage, and you are right. But (back in the olden days) Brat used to do, like, whatever it wanted in this scenario, but while it may poke one's pride to think of how "pure" and "good" a language like that is, in the end it is silly to not have normal mathematical precedence for basic things. So My Dear Aunt Sally will remain satisfied. For now.
One may wonder what precedence in Brat is like. That's cool. It's like this, from high to low:
^
% / *
+ -
>> <<
> < >= <=
!~ ~ != == <=>
&&
||
Everything else
Okay, that's cool and all. But these things are still a little weird to me. What about short-circuiting, then? Is it out the window? No, of course not. The normal short-circuiting definition for ||
and &&
works, you just need to put the right hand side into a function, that's all.
true || { p "not true!" } # Does not print anything
Unary operators are pretty much like binary operators except like half as much. Also, unary operators go in front of their object, not making a mess in the middle of everything like binary operators do.
string.! = { "BANG! #{my}" }
p !"You are dead."
Unary operators are kind of weird, though, because of parsing and whatnot. Use as sparingly but as awesomely as possible. Also, unary operators are why variable names begin with a letter. Blame it one them.
Technically, these are defined as methods on object
, but we can ignore that for now.
true?
, false?
, and null?
all operate in the same way. They can take 0-3 arguments, like so:
(2 > 1).true? #Returns true
true? 2 > 1 #Also returns true
true? 2 > 1, { p "hello" } #Prints "hello"
true? 1 > 2, { p "hello" }, { p "goodbye" } #Prints "goodbye"
This is a good place to demonstrate the flexibility of Brat's syntax, though. Here's another example:
false? 1 + 1 == 2
{ p "Freedom is Slavery!" }
{ p "Yay, 1 + 1 still equals 2" }
But always remember Brat's greediness! These are just function calls, so be careful how the condition and return values are specified. It is safest to put them all inside functions.
[To whom it may concern: this section needs love, love, love. And that is all it needs.]
Ew, guts.
Brat is written in Brat and also Lua. It compiles to Lua and runs on MoonJIT. That means your code might get Just-In-Time compiled down to machine code. So sometimes it can be quite fast.
Brat Executable
Brat comes with a small shell script called brat
which runs really just runs bin/brat
which is a Lua program. bin/brat
sets up the environment for Brat and can do things like start Interactive Brat or compile/run Brat code in files
Some options:
-
-
Reads Brat code from standard input and then executes it at EOF -
-!
Reads in Lua code from standard input and then executes it at EOF
With no arguments, Interactive Brat will start up. Anything else is assumed to be a file name.
Parser
Brat's parser and compiler is all written in Brat.
Brat uses a parsing expression grammar defined here.
The parser and compiler live in stdlib/parser
Core
There is a big file called core.lua
which implements the parts of Brat that the parser doesn't. Mostly, its a collection of objects available by default in Brat.
Standard Library
A tiny standard library lives in stdlib/
. It does very little at the moment.
Tests
Brat comes with a test suite, which is now implemented entirely in Brat. Predictably, it lives in tests/
.
To run all the tests, run brat test/test.brat
from the root directory of the Brat files.
1 May not be true.