fix more syntax v2 bugs, update docs

This commit is contained in:
liquidex 2024-09-01 18:54:38 +02:00
parent bf37d7305c
commit d1a6fb364e
11 changed files with 544 additions and 528 deletions

1
Cargo.lock generated
View file

@ -570,6 +570,7 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
name = "haku"
version = "0.1.0"
dependencies = [
"log",
"tiny-skia",
]

View file

@ -465,6 +465,8 @@ unsafe extern "C" fn haku_render_value(
debug!("resetting exception");
instance.exception = None;
debug!("will render value: {:?}", instance.value);
let pixmap_locked = &mut (*pixmap).pixmap;
let mut renderer = Renderer::new(

View file

@ -4,4 +4,5 @@ version = "0.1.0"
edition = "2021"
[dependencies]
log.workspace = true
tiny-skia = { version = "0.11.4", default-features = false, features = ["no-std-float"] }

View file

@ -13,6 +13,7 @@ pub enum Opcode {
False,
True,
Number, // (float: f32)
Rgba, // (r: u8, g: u8, b: u8, a: u8)
// Duplicate existing values.
/// Push a value relative to the bottom of the current stack window.
@ -26,6 +27,9 @@ pub enum Opcode {
/// Set the value of a definition.
SetDef, // (index: u16)
// Create lists.
List, // (len: u16)
// Create literal functions.
Function, // (params: u8, then: u16), at `then`: (local_count: u8, capture_count: u8, captures: [(source: u8, index: u8); capture_count])

View file

@ -4,6 +4,7 @@ use core::{
};
use alloc::vec::Vec;
use log::info;
use crate::{
ast::{Ast, NodeId, NodeKind},
@ -98,12 +99,11 @@ pub fn compile_expr<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId)
// as they may also contain commas and other trivia.
NodeKind::Param => unreachable!("Param node should never be emitted"),
NodeKind::Color => unsupported(c, src, node_id, "color literals are not implemented yet"),
NodeKind::Ident => compile_ident(c, src, node_id),
NodeKind::Number => compile_number(c, src, node_id),
NodeKind::Tag => compile_tag(c, src, node_id),
NodeKind::List => unsupported(c, src, node_id, "list literals are not implemented yet"),
NodeKind::Number => compile_number(c, src, node_id),
NodeKind::Color => compile_color(c, src, node_id),
NodeKind::List => compile_list(c, src, node_id),
NodeKind::Unary => compile_unary(c, src, node_id),
NodeKind::Binary => compile_binary(c, src, node_id),
@ -200,18 +200,6 @@ fn compile_ident<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) ->
Ok(())
}
fn compile_number(c: &mut Compiler, src: &Source, node_id: NodeId) -> CompileResult {
let literal = src.ast.span(node_id).slice(src.code);
let float: f32 = literal
.parse()
.expect("the parser should've gotten us a string parsable by the stdlib");
c.chunk.emit_opcode(Opcode::Number)?;
c.chunk.emit_f32(float)?;
Ok(())
}
fn compile_tag(c: &mut Compiler, src: &Source, node_id: NodeId) -> CompileResult {
let tag = src.ast.span(node_id).slice(src.code);
@ -230,6 +218,79 @@ fn compile_tag(c: &mut Compiler, src: &Source, node_id: NodeId) -> CompileResult
Ok(())
}
fn compile_number(c: &mut Compiler, src: &Source, node_id: NodeId) -> CompileResult {
let literal = src.ast.span(node_id).slice(src.code);
if let Ok(float) = literal.parse() {
c.chunk.emit_opcode(Opcode::Number)?;
c.chunk.emit_f32(float)?;
}
Ok(())
}
fn compile_color(c: &mut Compiler, src: &Source, node_id: NodeId) -> CompileResult {
let literal = src.ast.span(node_id).slice(src.code);
let hex = &literal[1..];
let bytes: [u8; 4] = u32::from_str_radix(hex, 16)
.ok()
.and_then(|num| match hex.len() {
3 => Some([
(((num & 0xF00) >> 8) * 0x11) as u8,
(((num & 0x0F0) >> 4) * 0x11) as u8,
((num & 0x00F) * 0x11) as u8,
0xFF,
]),
4 => Some([
(((num & 0xF000) >> 12) * 0x11) as u8,
(((num & 0x0F00) >> 8) * 0x11) as u8,
(((num & 0x00F0) >> 4) * 0x11) as u8,
((num & 0x000F) * 0x11) as u8,
]),
6 => Some([
((num & 0xFF0000) >> 16) as u8,
((num & 0x00FF00) >> 8) as u8,
(num & 0x0000FF) as u8,
0xFF,
]),
8 => Some([
((num & 0xFF000000) >> 24) as u8,
((num & 0x00FF0000) >> 16) as u8,
((num & 0x0000FF00) >> 8) as u8,
(num & 0x000000FF) as u8,
]),
_ => None,
})
.unwrap_or([0, 0, 0, 0]);
c.chunk.emit_opcode(Opcode::Rgba)?;
c.chunk.emit_bytes(&bytes)?;
Ok(())
}
fn compile_list<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> CompileResult {
let mut walk = src.ast.walk(node_id);
let mut len = 0;
while let Some(expr) = walk.node() {
compile_expr(c, src, expr)?;
len += 1;
}
let len = u16::try_from(len).unwrap_or_else(|_| {
// For all practical intents and purposes, this should never happen---you'll most likely
// run into the chunk length limit first.
c.emit(Diagnostic::error(src.ast.span(node_id), "list is too long"));
0
});
c.chunk.emit_opcode(Opcode::List)?;
c.chunk.emit_u16(len)?;
Ok(())
}
fn compile_unary<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> CompileResult {
let mut walk = src.ast.walk(node_id);
let Some(op) = walk.node() else { return Ok(()) };

View file

@ -17,16 +17,16 @@ impl<'a> Lexer<'a> {
pub fn new(lexis: Lexis, input: &'a SourceCode) -> Self {
Self {
lexis,
diagnostics: Vec::new(),
diagnostics: Vec::with_capacity(16),
input,
position: 0,
}
}
fn current(&self) -> char {
self.input[self.position as usize..]
.chars()
.next()
self.input
.get(self.position as usize..)
.and_then(|s| s.chars().next())
.unwrap_or('\0')
}
@ -140,7 +140,7 @@ fn whitespace_and_comments(l: &mut Lexer<'_>) {
let position = l.position;
l.advance();
if l.current() == '-' {
while l.current() != '\n' {
while l.current() != '\n' && l.current() != '\0' {
l.advance();
}
} else {

View file

@ -143,7 +143,8 @@ pub mod fns {
0x88 Nary "rgbaB" => rgba_b,
0x89 Nary "rgbaA" => rgba_a,
0x90 Nary "list" => list,
// NOTE: Not used right now, has been replaced with Opcode::List.
0x90 Nary "list (unused)" => list,
0xc0 Nary "toShape" => to_shape_f,
0xc1 Nary "line" => line,

View file

@ -5,11 +5,12 @@ use core::{
};
use alloc::{string::String, vec::Vec};
use log::info;
use crate::{
bytecode::{self, Defs, Opcode, CAPTURE_CAPTURE, CAPTURE_LOCAL},
system::{ChunkId, System},
value::{BytecodeLoc, Closure, FunctionName, Ref, RefId, Rgba, Value, Vec4},
value::{BytecodeLoc, Closure, FunctionName, List, Ref, RefId, Rgba, Value, Vec4},
};
pub struct VmLimits {
@ -208,6 +209,19 @@ impl Vm {
self.push(Value::Number(x))?;
}
Opcode::Rgba => {
let r = chunk.read_u8(&mut pc)?;
let g = chunk.read_u8(&mut pc)?;
let b = chunk.read_u8(&mut pc)?;
let a = chunk.read_u8(&mut pc)?;
self.push(Value::Rgba(Rgba {
r: r as f32 / 255.0,
g: g as f32 / 255.0,
b: b as f32 / 255.0,
a: a as f32 / 255.0,
}))?;
}
Opcode::Local => {
let index = chunk.read_u8(&mut pc)? as usize;
let value = self.get(bottom + index)?;
@ -246,6 +260,20 @@ impl Vm {
}
}
Opcode::List => {
let len = chunk.read_u16(&mut pc)? as usize;
let bottom = self.stack.len().checked_sub(len).ok_or_else(|| {
self.create_exception(
"corrupted bytecode (list has more elements than stack)",
)
})?;
let elements = self.stack[bottom..].to_vec();
self.stack.resize_with(bottom, || unreachable!());
self.track_array(&elements)?;
let id = self.create_ref(Ref::List(List { elements }))?;
self.push(Value::Ref(id))?;
}
Opcode::Function => {
let param_count = chunk.read_u8(&mut pc)?;
let then = chunk.read_u16(&mut pc)? as usize;

View file

@ -27,7 +27,7 @@ In case you edited anything in the input box on the right, paste the following t
-- Try playing around with the numbers,
-- and see what happens!
stroke 8 (rgba 0 0 0 1) (vec 0 0)
stroke 8 #000 (vec 0 0)
```
rakugaki is a drawing program for digital scribbles and other pieces of art.
@ -78,36 +78,52 @@ Theoretically, this would mean brushes are very limited.
After all, if we're only limited to drawing single scribbles, wouldn't that mean a brush can only draw a single shape?
But the magical part is that you can _compose scribbles together._
If you want to draw multiple scribbles, you can wrap them into a `list`:
If you want to draw multiple scribbles, you can wrap them into a list, which we denote with square brackets `[]`:
```haku
; Draw two colorful dots instead of one!
(list
(stroke 8 (rgba 1.0 0.0 0.0 1.0) (vec 4 0))
(stroke 8 (rgba 0.0 0.0 1.0 1.0) (vec (- 4) 0)))
-- Draw two colorful dots instead of one!
[
stroke 8 #F00 (vec 4 0)
stroke 8 #00F (vec (-4) 0))
]
```
::: aside
This is kind of weird, but to negate a number in haku, you have to write `(- number)`, with all the parentheses and spaces in place.
haku uses the syntax `-- OwO` for _comments_---human-readable pieces of text that are ignored by the compiler.
A comment begins with `--`, and ends at the end of a line.
You'll understand why later!
They're pretty useful for making your code more understandable!
After all, we don't speak programming languages natively.
:::
And what's even crazier is that you can composes lists _further_---you can make a list of lists, and rakugaki will be happy with that!
And what's even crazier is that you can compose lists _further_---you can make a list of lists, and rakugaki will be happy with that!
It'll draw the first inner list, which contains two scribbles, and then it'll draw the second inner list, which contains two scribbles.
```haku
(list
(list
(stroke 8 (rgba 1.0 0.0 0.0 1.0) (vec 4 (- 4)))
(stroke 8 (rgba 0.0 0.0 1.0 1.0) (vec (- 4) (- 4))))
(list
(stroke 8 (rgba 1.0 1.0 0.0 1.0) (vec 4 4))
(stroke 8 (rgba 0.0 1.0 1.0 1.0) (vec (- 4) 4))))
[
[
stroke 8 #F00 (vec 4 (-4))
stroke 8 #00F (vec (-4) (-4))
]
[
stroke 8 #FF0 (vec 4 4)
stroke 8 #0FF (vec (-4) 4)
]
]
```
::: aside
Another weird thing: when negating a number, you have to put it in parentheses.
This is because haku does not see your spaces---`vec -4`, `vec - 4`, and `vec-4` all mean the same thing!
In this case, it will always choose the 2nd interpretation---vec minus four.
So to make it interpret our minus four as, well, _minus four_, we need to enclose it in parentheses.
:::
This might seem useless, but it's a really useful property in computer programs.
It essentially means you can snap pieces together like Lego bricks!
@ -120,122 +136,65 @@ Therefore, scribbles that are listed later will be drawn on top of scribbles tha
Anyways!
## So what's this ceremony with all the parentheses and such?
::: aside
I'm working on an improved syntax for haku that will get rid of the prevalence of parentheses.
:::
## So what's this ceremony with all the words and symbols?
Recall that super simple brush from before...
```haku
(stroke
8
(rgba 0.0 0.0 0.0 1.0)
(vec))
stroke 8 #000 (vec 0 0)
```
It'll be best to explain what happens here by example, so let's have a look at that singular `rgba` line.
This reads as "a stroke that's 8 pixels wide, has the color `#000`, and is drawn at the point `(0, 0)` relative to the mouse cursor."
```haku
(rgba 0.0 0.0 0.0 1.0)
```
All these symbols are very meaningful to haku.
If you reorder or remove any one of them, your brush isn't going to work!
This is the syntax haku uses for representing RGBA colors.
- Reading from left to right, we start with `stroke`.\
`stroke` is a _function_---a recipe for producing data!\
haku has [many such built-in recipes](/docs/system.html).
`stroke` is one of them.
It looks simple enough, but we can dismantle it even further.
- Each function requires some amount of _arguments_.
These are the ingredients that will be used to produce our piece of data.\
In haku, we specify the arguments to a function by listing them on the same line as the function's name, one after another, separated by spaces.
- There's the word `rgba`.
- The first ingredient we need for a `stroke` is its _thickness_.
This is a plain old number, counted in pixels. We say we want a stroke of thickness `8`.
- There are four number values, `0.0`, `0.0`, `0.0`, and `1.0`.
- The second ingredient is the stroke's _color_.
haku uses the familiar hex code syntax `#RRGGBB` for colors, but it allows writing `#RGB` for brevity---`#08F` is the same as `#0088FF`.\
You can also specify an alpha channel, for transparent colors---`#RRGGBBAA`, or `#RGBA`.
- All of this is wrapped in parentheses.
- The third ingredient is the stroke's _position_.
[*The parentheses are _very_ significant.*]{id=what-happens-if-you-remove-the-parentheses}
If we remove them, our brush no longer runs, and instead fails with an error.
```haku
(stroke
8
rgba 0.0 0.0 0.0 1.0
(vec))
```
```
14..18: undefined variable
```
So what's happening here?
Put shortly, the parentheses are telling haku to _call a function._
This is computer-speak for asking haku to _do_ something with data.
In case of `(rgba 0.0 0.0 0.0 1.0)`, we're telling haku to create an RGBA color out of four numbers, which signify the Red, Green, Blue, and Alpha channels of our color.
In computer-speak, `rgba` is a _function_, which we _call_ with _four number arguments_, and it _returns_ an RGBA color for us to use.
If you've studied math in school, you can visualize haku functions as being very similar to mathematical functions.
Both take in arguments, and both can be substituted for some results.
Both also have domains---just as it's invalid to take the square root of a negative number, it is invalid to call `rgba` with something other than four numbers.
haku defines many such ready-to-use functions.
These functions make up the _system library_---just like in a real world library there are lots of books, the haku system library has lots of functions.
We call it the _system_ library, because together with the programming language, they make up a system of rules, which we collectively call _haku_.
Philosophy aside, you can reference all the functions from the system library in the [system library reference](/docs/system.html).
Anyways, to finish off dismantling that entire `stroke` example---
```haku
(stroke
8
(rgba 0.0 0.0 0.0 1.0)
(vec))
```
Just like `rgba`, `vec` is also a system function.
It's a function which produces a mathematical _vector_, which, in simple terms, is just a list of some numbers.
Positions in haku are represented using mathematical _vectors_, which, when broken down into pieces, are just lists of some numbers.
haku vectors however are a little more constrained, because they always contain _four_ numbers---this makes them _four-dimensional_.
We call these four numbers X, Y, Z, and W respectively.
Four is a useful number of dimensions to have, because it lets us do 3D math---which technically isn't built into haku, but if you want it, it's there.
For most practical purposes, we'll only be using the first _two_ of the four dimensions though---X and Y.
This is because the wall is a 2D space---it's a flat surface with no depth.
We most commonly use vectors to represent points on the wall.
These points are always relative to some origin, which is located at the coordinates `(0, 0)`.
In case of brushes, we consider the origin to be the located at the tip of the mouse cursor.
It's important to know though that vectors don't mean much _by themselves_---rakugaki just chooses them to represent points on the wall, but in a flat 2D space, all points need to be relative to some _origin_---the vector `(0, 0)`.
In brushes, this position is at the tip of the mouse cursor.
Positive X coordinates go rightwards, and positive Y coordinates go downwards.
Likewise, negative X coordinates go leftwards, and negative Y coordinates go upwards.
::: aside
---
Many programming languages (and math libraries) make points a different type of data from vectors.
Going back to the example though, `vec` is yet another function, except instead of producing strokes, it produces vectors!
The reason for this is that it doesn't make sense to, for example, add points together.
You can only add a vector to a point, which gives you a point, or a vector to a vector, which gives you a vector, but not a point to a point---because that operation simply doesn't make sense mathematically.
Note how it's parenthesized though---recall that function arguments are separated with spaces, so if we didn't parenthesize the `vec`, we'd end up passing `vec`, `0`, and `0` back to `stroke`---which is far from what we want!
haku goes with the simpler way of using one data type, because I've never found this to be useful in practice.
In theory it can prevent bugs, but usually these bugs are easy enough to see and squash immediately.
And with all that, we let haku mix all the ingredients together, and get a black dot under the cursor.
:::
```haku
stroke 8 #000 (vec 0 0)
```
`(vec)` is a short way of saying, "a vector with all dimensions equal to zero."
We could just as well write `(vec 0 0 0 0)`, but that's no fun to type every single time you'd like a zero vector.
Therefore, in case of `vec`, haku allows you to omit any of the four arguments, and initializes them to zero for you.
And lastly, there's the `stroke` function.
It turns a stroke thickness (number,) color (`rgba`,) and position (vector,) into a scribble that we can draw on the wall.
Just like cooking!
You add eggs, milk, flour, some salt, and some sugar, mix them up, and you get pancake batter.
In haku, you add thickness, color, and position, mix them together into a `stroke`, and get a little colored square on the wall!
Nice!
## Shapes
@ -250,15 +209,10 @@ Right now haku supports two additional shapes: rectangles and circles.
You can try them out by playing with this brush!
```haku
(list
(stroke
8
(rgba 1.0 0.0 0.0 1.0)
(circle (- 16) 0 16))
(stroke
8
(rgba 0.0 0.0 1.0 1.0)
(rect 0 (- 16) 32 32)))
[
stroke 8 #F00 (circle (-16) 0 16)
stroke 8 #00F (rect 0 (-16) 32 32)
]
```
::: aside
@ -272,7 +226,7 @@ But let's not go down that rabbit hole.
- `circle`s are made up of an X position, Y position, and radius.
- `rect`s are made up of the (X and Y) position of their top-left corner, and a size (width and height).\
Our example produces a square, because the rectangle's width and height are the same!
Our example produces a square, because the rectangle's width and height are equal!
## Programming in haku
@ -282,19 +236,21 @@ But if describing data was all we ever wanted, we could've just used any ol' dra
Remember that example from before?
```haku
(list
(stroke 8 (rgba 1.0 0.0 0.0 1.0) (vec 4 0))
(stroke 8 (rgba 0.0 0.0 1.0 1.0) (vec (- 4) 0)))
[
stroke 8 #F00 (vec 4 0)
stroke 8 #00F (vec (-4) 0)
]
```
It has quite a bit of repetition in it.
If we wanted to change the size of the points, we'd need to first update the stroke thickness...
```haku
(list
; ↓
(stroke 4 (rgba 1.0 0.0 0.0 1.0) (vec 4 0))
(stroke 4 (rgba 0.0 0.0 1.0 1.0) (vec (- 4) 0)))
[
stroke 4 #F00 (vec 4 0)
stroke 4 #00F (vec (-4) 0)
---
]
```
...twice of course, because we have two scribbles.
@ -302,17 +258,19 @@ But now there's a gap between our points!
So we also have to update their positions.
```haku
(list
; ↓
(stroke 4 (rgba 1.0 0.0 0.0 1.0) (vec 2 0))
; ↓
(stroke 4 (rgba 0.0 0.0 1.0 1.0) (vec (- 2) 0)))
[
stroke 4 #F00 (vec 2 0)
---
stroke 4 #00F (vec (-2) 0)
--
]
```
Now imagine if we had four of those points.
That's quite a lot of copy-pasting for such a simple thing!
Luckily, haku has a solution for this: we can give a _name_ to a piece of data by using a _definition_, and then refer to that piece of data using that name we chose.
Definitions are called _defs_ in short.
::: aside
@ -326,48 +284,47 @@ Once you define a name, its associated data stays the same throughout the entire
So we can define `thickness` to be `4`, and then use it in our scribbles.
```haku
(def thickness 4)
thickness = 4
(list
(stroke thickness (rgba 1.0 0.0 0.0 1.0) (vec 2 0))
(stroke thickness (rgba 0.0 0.0 1.0 1.0) (vec (- 2) 0)))
; ^^^^^^^^^
[
stroke thickness #F00 (vec 2 0)
stroke thickness #00F (vec (-2) 0)
---------
]
```
`(def name data)` is a special construction in haku that tells the language "whenever we say `name`, we mean `data`."
Unlike `list` or `stroke`, it is _not_ a function!
`name = data` is a special operator in haku that tells the language "whenever we say `name`, we mean `data`."
We cannot put `def`s in arbitrary places in our program, because it wouldn't make sense.
What does it mean to have a stroke whose thickness is `(def meow 5)`?
We cannot use it in arbitrary places in our program, because it wouldn't make sense.
What does it mean to have a stroke whose thickness is `meow = 5`?
To keep a consistent program structure, haku also forces all your `def`s to appear _before_ your scribble.
You can think of the `def`s as a list of ingredients for the final scribble.
To keep a consistent program structure, haku also forces all your defs to appear _before_ your scribble.
You can think of the defs as a list of ingredients for the final scribble.
Reading the ingredients can give you context as to what you're gonna be cooking, so it's useful to have them first!
Anyways, we can likewise replace our `2` constants with a `def`:
Anyways, we can likewise replace our `2` constants with a def:
```haku
(def thickness 4)
(def x-offset 2)
thickness = 4
xOffset = 2
(list
(stroke thickness (rgba 1.0 0.0 0.0 1.0) (vec x-offset 0))
(stroke thickness (rgba 0.0 0.0 1.0 1.0) (vec (- x-offset) 0)))
; ^^^^^^^^^
[
stroke thickness #F00 (vec xOffset 0)
stroke thickness #00F (vec (-xOffset) 0)
---------
]
```
I chose the name `x-offset` because it demonstrates that names don't need to be made up of letters only.
In fact, haku will be happy with any of the following characters:
- lowercase and uppercase letters
- numbers (as long as your name doesn't start with a number)
- dashes `-` and underscores `_`
- various symbols: `+`, `*`, `/`, `\`, `^`, `!`, `=`, `<`, `>`---these are mainly used in math functions like `+`, but nothing prevents you from using them yourself!
Note how in haku, names may not contain spaces.
We cannot have a variable called `x offset`, so we choose `xOffset` instead.
This naming convention is known as `camelCase`, and is used everywhere throughout the haku system library.
::: aside
Fancy symbols in names are typical of languages in the [Lisp](https://en.wikipedia.org/wiki/Lisp_%28programming_language%29) family.
After all, since there are so few syntactic constructions---only literal values and lists---why not make the rest of the characters on your keyboard available for naming things?
Of note is that haku names also cannot start with an uppercase letter.
It's reserved syntax for the future.
Right now the only names that start with an uppercase letter are the two booleans, `True` and `False`.
:::
@ -375,24 +332,27 @@ But now there's a problem.
If we change our `thickness` back to `8`, our points will overlap!
```haku
; ↓
(def thickness 8)
(def x-offset 2)
thickness = 8
---
xOffset = 2
(list
(stroke thickness (rgba 1.0 0.0 0.0 1.0) (vec x-offset 0))
(stroke thickness (rgba 0.0 0.0 1.0 1.0) (vec (- x-offset) 0)))
[
stroke thickness #F00 (vec xOffset 0)
stroke thickness #00F (vec (-xOffset) 0)
]
```
So we'll make our `x-offset` calculated dynamically from the `thickness`, to not have to update it every time.
So we'll make our `xOffset` calculated dynamically from the `thickness`, to not have to update it every time.
```haku
(def thickness 8)
(def x-offset (/ thickness 2))
thickness = 8
xOffset = thickness / 2
-------------
(list
(stroke thickness (rgba 1.0 0.0 0.0 1.0) (vec x-offset 0))
(stroke thickness (rgba 0.0 0.0 1.0 1.0) (vec (- x-offset) 0)))
[
stroke thickness #F00 (vec xOffset 0)
stroke thickness #00F (vec (-xOffset) 0)
]
```
Try playing with the `thickness` now!
@ -404,18 +364,14 @@ So far we've only been dealing with strokes.
So why not switch it up a little and _fill in_ a shape?
```haku
(fill
(rgba 0 0 0 1)
(circle 0 0 16))
fill #000 (circle 0 0 16)
```
How about... some transparency?
Recall that `rgba` has a fourth Alpha parameter---in image manipulation, this parameter is used to control the _opacity_ of a color.
Recall that colors can have an alpha component, so let's try using that!
```haku
(fill
(rgba 0 0 0 0.1)
(circle 0 0 16))
fill #0001 (circle 0 0 16)
```
If you play around with this brush, you'll notice how the circles blend together really nicely.
@ -424,42 +380,32 @@ That's the power of Alpha!
Now let's see what happens if we draw two such circles on top of each other---one bigger, one smaller.
```haku
(list
(fill
(rgba 0 0 0 0.1)
(circle 0 0 16))
(fill
(rgba 0 0 0 0.1)
(circle 0 0 32)))
[
fill #0001 (circle 0 0 16)
fill #0001 (circle 0 0 32)
]
```
How about four?
```haku
(list
(fill
(rgba 0 0 0 0.1)
(circle 0 0 8))
(fill
(rgba 0 0 0 0.1)
(circle 0 0 16))
(fill
(rgba 0 0 0 0.1)
(circle 0 0 24))
(fill
(rgba 0 0 0 0.1)
(circle 0 0 32)))
[
fill #0001 (circle 0 0 8)
fill #0001 (circle 0 0 16)
fill #0001 (circle 0 0 24)
fill #0001 (circle 0 0 32)
]
```
Okay, this is starting to look interesting, but it's also getting super unwieldy code-wise!
I mean, just look at these repeated lines...
Doesn't that remind you of that previous code example?
Could there be some way to cleverly use `def`s to make it more readable?
Could there be some way to cleverly use defs to make it more readable?
...Well, the problem here's that the values vary, while `def`s are constant!
So no `def` in the world is going to save us here.
...Well, the problem here's that the values vary, while defs are constant!
So no def in the world is going to save us here.
But what if we could `def` some _code_, and then weave our changing values into that?
But what if we could def some _code_, and then weave our changing values into that?
Or, maybe in other words, list a bunch of values, and then transform them into something else?
...We already have a tool for that!
@ -469,17 +415,14 @@ Or, maybe in other words, list a bunch of values, and then transform them into s
Just like haku defines a set of _system_ functions, we can create and define _our own_ functions too!
In haku, functions are data like anything else.
We create them using `fn`, which is yet another magical construct like `def`.
Except because functions are real data, they're not as restricted as `def`s are---they behave like any other piece of data, such as numbers.
We can give them names with `def`s, or we can pass them into other functions for further manipulation.
We create them using the syntax `\x -> y`.
Because they are data like anything else, we can give them names with defs, or we can pass them into other functions for further manipulation.
::: aside
Actually, system functions are kind of special.
For performance reasons, (and because I was hasty to get a working prototype,) they cannot be passed as arguments to other functions.
It's why [the error message here is so cryptic](#what-happens-if-you-remove-the-parentheses).
That'll need fixing!
:::
@ -487,34 +430,33 @@ That'll need fixing!
Either way, let's define a function that'll make us those circles!
```haku
(def splat
(fn (radius)
(fill
(rgba 0 0 0 0.1)
(circle 0 0 radius))))
splat = \radius ->
fill #0001 (circle 0 0 radius)
(list
(splat 8)
(splat 16)
(splat 24)
(splat 32))
[
splat 8
splat 16
splat 24
splat 32
]
```
That's a lot nicer, isn't it---a template for our circles is neatly defined in a single place, and all we do is reuse it, each time with a different `radius`.
To dismantle that `fn` literal...
To dismantle that weird `\` syntax...
- We have the special word `fn`, which is short for _*f*u*n*ction_.
- The character `\` is a short way of saying _function of_.
It's supposed to resemble the Greek letter λ, but be easier to type on our antiquated ASCII keyboards.
- We have a list of _parameters_.
- After `\`, we have a list of _parameters_.
Parameters are the names we give to a function's arguments---for a function call `(splat 8)`, we need the function to have a name for that `8` datum that gets passed to it.
Parameters are the names we give to a function's arguments---for a function call `splat 8`, we need the function to have a name for that `8` datum that gets passed to it.
Otherwise it has no way to use it!
A function can have an arbitrary number of parameters listed, and that many parameters _must_ be passed to it.
A function can have an arbitrary number of parameters listed, separated by commas, and that many parameters _must_ be passed to it.
Otherwise your brush will fail with an error!
- And lastly, we have the function's result.
- And lastly, after an arrow `->`, we have the function's result.
Note that a function can only have _one_ result, just like a brush can only have one scribble.
@ -533,21 +475,19 @@ haku limits the use of overloading to system functions for simplicity---adding o
Since these transparent circles are so much easier to draw now, let's make a few more of them!
```haku
(def splat
(fn (radius)
(fill
(rgba 0 0 0 0.1)
(circle 0 0 radius))))
splat = \radius ->
fill #0001 (circle 0 0 radius)
(list
(splat 8)
(splat 16)
(splat 24)
(splat 32)
(splat 40)
(splat 48)
(splat 56)
(splat 64))
[
splat 8
splat 16
splat 24
splat 32
splat 40
splat 48
splat 56
splat 64
]
```
Okay, I'll admit this is getting kind of dumb.
@ -580,19 +520,16 @@ Until some threshold is reached, in which case we just make a single circle.
The first part is easy to do: haku allows us to define a function that calls itself without making any fuss.
```haku
(def splat
(fn (radius)
(fill
(rgba 0 0 0 0.1)
(circle 0 0 radius))))
splat = \radius ->
fill #0001 (circle 0 0 radius)
(def airbrush
(fn (size)
(list
(splat size)
(airbrush (- size 8)))))
airbrush = \size ->
[
splat size
airbrush (size - 8)
]
(airbrush 64) ; sounds like some Nintendo 64 game about graffiti, lol.
airbrush 64 -- sounds like some Nintendo 64 game about graffiti, lol.
```
But...
@ -618,13 +555,15 @@ We call this act of switching execution paths _branching_.
Try this out---change the `radius`, and observe how your brush changes color once you set it beyond 16:
```haku
(def radius 8)
radius = 8
(fill
(if (< radius 16)
(rgba 0 0 1 1)
(rgba 1 0 0 1))
(circle 0 0 radius))
color =
if (radius < 16)
#00F
else
#F00
fill color (circle 0 0 radius)
```
- `<` is a function that produces `true` if the second argument is a smaller number than the first argument.
@ -639,21 +578,19 @@ An `if` only calculates the argument it needs to produce the result.
This allows us to use it to prevent unbounded recursion in our `airbrush` example.
```haku
(def splat
(fn (radius)
(fill
(rgba 0 0 0 0.1)
(circle 0 0 radius))))
splat = \radius ->
fill #0001 (circle 0 0 radius)
(def airbrush
(fn (size)
(if (> size 0)
(list
(splat size)
(airbrush (- size 8)))
(list))))
airbrush = \size ->
if (size > 0)
[
splat size
airbrush (size - 8)
]
else
[]
(airbrush 64)
airbrush 64
```
Neat!
@ -666,43 +603,39 @@ But the airbrush still looks super primitive.
Let's try increasing the fidelity by doing smaller steps!
```haku
(def splat
(fn (radius)
(fill
(rgba 0 0 0 0.1)
(circle 0 0 radius))))
splat = \radius ->
fill #0001 (circle 0 0 radius)
(def airbrush
(fn (size)
(if (> size 0)
(list
(splat size)
; ↓
(airbrush (- size 1)))
(list))))
airbrush = \size ->
if (size > 0)
[
splat size
airbrush (size - 1)
---
]
else
[]
(airbrush 64)
airbrush 64
```
Well... sure, that's just a black blob with a slight gradient on the outer edge, so let's decrease the opacity.
```haku
(def splat
(fn (radius)
(fill
; ↓
(rgba 0 0 0 0.01)
(circle 0 0 radius))))
splat = \radius ->
fill #00000004 (circle 0 0 radius)
---------
(def airbrush
(fn (size)
(if (> size 0)
(list
(splat size)
(airbrush (- size 1)))
(list))))
airbrush = \size ->
if (size > 0)
[
splat size
airbrush (size - 1)
]
else
[]
(airbrush 64)
airbrush 64
```
Looks good as a single dot, but if you try drawing with it... it's gray??

View file

@ -9,230 +9,243 @@ Each function comes with a _signature description_.
These descriptions read like this:
```haku
(stroke
(thickness number)
(color rgba)
(position vec)
scribble)
stroke
thickness : number
color : rgba
position : vec
-> scribble
```
The first element is always the function's name - in this case `stroke`.
Following this are `(argument-name argument-type)` pairs, which describe the arguments that need to be passed to the function.
Following this are `argumentName : argumentType` pairs, which describe the arguments that need to be passed to the function.
The last element is always the type of data this function produces.
Function names which are made up of symbols instead of letters are _operators_.
Operators may have one or two arguments, where one argument corresponds to a prefix form `-x`, and two arguments correspond to an infix form `x - y`.
Note that this documentation lists a unary and binary operator of the same spelling as _two separate functions_, not overloads of a single function.
The argument name usually does not matter when calling the function - it is only used for documentation purposes.
The one exception is arguments called `...`, which signify that zero or more arguments can be passed to the function at that position.
(Currently there are no functions that accept any number of arguments, though.)
The argument _type_ however is important.
If you try to use a function with the wrong type of value as its argument, it will fail with an error.
For example, consider a brush where we pass a number as `stroke`'s `color` and `position` arguments.
```haku
(stroke 1 1 1)
stroke 1 1 1
```
This brush will fail to render, since `stroke` expects an `rgba` as its 2nd argument.
With that said, there are several types of values in haku that can be passed into, and returned by functions.
- `*` - special type used to signify that any value may be passed into the function.
- `_` - special type used to signify that any value may be passed into the function.
- `()` - also known as _nil_, means _no value._
- `boolean` - either `false` or `true`. Indicates truth or falsehood, used in `if` conditions.
- `boolean` - either `False` or `True`. Indicates truth or falsehood, used in `if` conditions.
- `number` - a real number, with 32 bits of precision.
- `vec` - a 4-dimensional vector, composed of four `number`s.
- `rgba` - an RGBA color, composed of four `number`s.
- `fn` - a function, as returned by `(fn (x) x)` literals.
- `list` - a list of values, where each value can have a different type (even `list` itself.)
- `\a -> r` - a function taking in the parameter `a` and returning `r`, as returned by `\x -> x` literals.
- `list t` - a list of values, where each value is of the type `t`.
- `shape` - a mathematical shape.
- `shape-like` - anything that can be turned into a `shape` using `to-shape`.
- `shapeLike` - anything that can be turned into a `shape` using `toShape`.
- `scribble` - something that can be drawn on the wall.
Additionally, the syntax `(type-a type-b ...)` may be used to signify that one of the listed types is accepted or returned.
Additionally, the syntax `a | b` may be used to signify that one of the listed types is accepted or returned.
## Math
```haku
(+
(... number)
number)
-
a : number
-> number
```
`+` takes an arbitrary amount of arguments and sums them together.
When there are zero arguments, it returns `0`.
`-`, when used in its unary form `-x`, returns the number `x` with the opposite sign.
```haku
(-
(a number)
(... number)
number)
+
a : number
b : number
-> number
```
When there is only one argument, `-` returns `a` with the opposite sign.
Otherwise, it performs an arbitrary amount of subtractions from `a`, and returns the result.
Note that unlike `+` and `*`, at least one argument must be present.
haku does not have syntactic support for negative numbers - the proper way to negate a number is using this function `(- 1)`.
`+` adds two numbers together.
```haku
(*
(... number)
number)
-
a : number
b : number
-> number
```
`*` takes an arbitrary amount of arguments and multiplies them together.
When there are zero arguments, it returns `1`.
`-`, when used in its binary form `x - y`, subtracts two numbers from one another.
```haku
(/
(a number)
(... number)
number)
*
a : number
b : number
-> number
```
`/` returns `a` divided by all the numbers from `...`.
Note that unlike `+` and `*`, at least one argument must be present.
`*` multiplies two numbers together.
```haku
/
a : number
b : number
-> number
```
`/` divides a number by another number.
## Logic
The following functions are used to compare values and work with `boolean`s.
```haku
(not
(b *)
boolean)
!
a : _
-> boolean
```
If `b` is `()` or `false`, `not` returns `true`.
Otherwise it returns `false`.
If `b` is `()` or `False`, `not` returns `true`.
Otherwise it returns `False`.
```haku
(=
(a *)
(b *)
boolean)
==
a : _
b : _
-> boolean
(<>
(a *)
(b *)
boolean)
!=
a : _
b : _
-> boolean
```
`=` returns `true` if `a` and `b` are equal.
`==` returns `True` if `a` and `b` are equal.
Whether two values are considered equal depends on their type:
- If the type of the two values differs, `false` is returned.
- If the type of the two values differs, `False` is returned.
- If the two values are `number`s:
- If any of the values are `NaN`, `false` is returned.
- Otherwise `true` is returned if the two numbers have the exact same bit representation.
- If any of the values are `NaN`, `False` is returned.
- Otherwise `True` is returned if the two numbers have the exact same bit representation.
- If the two values are `vec`s, `true` is returned if each of their `number` components is equal to each other using the rules above.
- If the two values are `vec`s, `True` is returned if each of their `number` components is equal to each other using the rules above.
- Likewise with `rgba`s.
- All other types of values use _reference_ equality - `true` is returned only if `a` and `b` are located in the same place in memory.
- All other types of values use _reference_ equality - `True` is returned only if `a` and `b` are located in the same place in memory.
This more or less means that the values are considered equal if they are produced by the same call to a system function, in time.
`<>` returns `(not (= a b))`.
`!=` returns `!(a == b)`.
```haku
(<
(a *)
(b *)
boolean)
<
a : _
b : _
-> boolean
(<=
(a *)
(b *)
boolean)
<=
a : _
b : _
-> boolean
(>
(a *)
(b *)
boolean)
>
a : _
b : _
-> boolean
(>=
(a *)
(b *)
boolean)
>=
a : _
b : _
-> boolean
```
`<` returns `true` if `a` is less than `b`, and `<=` returns `true` if `a` is less than _or_ equal to `b`.
`<` returns `True` if `a` is less than `b`, and `<=` returns `True` if `a` is less than _or_ equal to `b`.
Order is only well-defined for numbers.
Other types may assume an arbitrary but consistent ordering - `()` may be less than `true`, or it may not be less than `true`, but this will not change between executions of the program.
Other types may assume an arbitrary but consistent ordering - `()` may be less than `True`, or it may not be less than `True`, but this will not change between executions of the program.
`(> a b)` is the same as `(< b a)`.
`(>= a b)` is the same as `(<= b a)`.
`a > b` is the same as `b < a`.
`a >= b` is the same as `b <= a`.
---
Note that `and` and `or` are currently missing from this list.
Note that `and` and `or` are currently missing from this list, but are reserved keywords.
You can implement them using regular functions as a replacement.
```haku
(def and
(fn (a b)
(if a (if b true false) false)))
boolAnd = \a, b ->
if (a)
if (b) True
else False
else False
(def or
(fn (a b)
(if a true (if b true false))))
boolOr = \a, b ->
if (a)
True
else
if (b) True
else False
```
## Vectors
```haku
(vec
vec)
vec
x : number
-> vec
(vec
(x number)
vec)
vec
x : number
y : number
-> vec
(vec
(x number)
(y number)
vec)
vec
x : number
y : number
z : number
-> vec
(vec
(x number)
(y number)
(z number)
vec)
(vec
(x number)
(y number)
(z number)
(w number)
vec)
vec
x : number
y : number
z : number
w : number
-> vec
```
Creates a new `vec` from zero to four number values.
Creates a new `vec` from one to four number values.
A `vec` always has four dimensions.
If any of the arguments are omitted, its corresponding dimension is initialized to zero.
```haku
(.x
(v vec)
number)
vecX
v : vec
-> number
(.y
(v vec)
number)
vecY
v : vec
-> number
(.z
(v vec)
number)
vecZ
v : vec
-> number
(.w
(v vec)
number)
vecW
v : vec
-> number
```
`.x`, `.y`, `.z`, and `.w` extract the individual components of a `vec`.
`vecX`, `vecY`, `vecZ`, and `vecW` extract the individual components of a `vec`.
---
@ -240,61 +253,54 @@ Note that mathematical operations are currently not defined for vectors.
You may define your own vector operations like so:
```haku
(def +v ; Vector addition
(fn (a b)
(vec
(+ (.x a) (.x b))
(+ (.y a) (.y b))
(+ (.z a) (.z b))
(+ (.w a) (.w b)))))
-- Vector addition
addv = \a, b ->
vec (vecX a + vecX b) (vecY a + vecY b) (vecZ a + vecZ b) (vecW a + vecW b)
; Likewise for subtraction, multiplication, and division.
-- Likewise for subtraction, multiplication, and division.
```
Note that haku-defined vector operations like these are more costly the more components they operate on.
Therefore, it's recommended to only define them for two dimensions, unless you really need more.
```haku
(def +v2 ; 2D vector addition
(fn (a b)
(vec
(+ (.x a) (.x b))
(+ (.y a) (.y b)))))
addv2 = \a, b ->
vec (vecX a + vecX b) (vecY a + vecY b)
```
## Colors
```haku
(rgba
(r number)
(g number)
(b number)
(a number)
rgba)
rgba
r : number
g : number
b : number
a : number
-> rgba
```
Creates a new `rgba` with the given color channels.
Note that unlike `vec`, all color channels have to be provided to form an `rgba`.
```haku
(.r
(color rgba)
number)
rgbaR
color : rgba
-> number
(.g
(color rgba)
number)
rgbaG
color : rgba
-> number
(.b
(color rgba)
number)
rgbaB
color : rgba
-> number
(.a
(color rgba)
number)
rgbaA
color : rgba
-> number
```
`.r`, `.g`, `.b`, and `.a` extract color channels out of an `rgba`.
`rgbaR`, `rgbaG`, `rgbaB`, `rgbaA` extract color channels out of an `rgba`.
---
@ -304,26 +310,20 @@ For example, consider multiplicatively blending two colors.
```haku
; This is how you can multiply two colors together.
(def *rgba
(fn (a b)
(rgba
(* (.r a) (.r b))
(* (.g a) (.g b))
(* (.b a) (.b b))
(* (.a a) (.a b)))))
mulRgba = \a, b ->
rgba (rgbaR a * rgbaR b) (rgbaG a * rgbaG b) (rgbaB a * rgbaB b) (rgbaA a * rgbaA b)
```
If haku represented colors using an 8-bit `0` to `255` range instead, to multiply two colors together, you would have to divide them by `255` to get them back into the correct range.
```haku
; NOTE: This example does NOT work correctly.
(def *rgba
(fn (a b)
(rgba
(/ (* (.r a) (.r b)) 255)
(/ (* (.g a) (.g b)) 255)
(/ (* (.b a) (.b b)) 255)
(/ (* (.a a) (.a b)) 255))))
mulRgba = \a, b ->
let red = (rgbaR a * rgbaR b) / 255
let green = (rgbaG a * rgbaG b) / 255
let blue = (rgbaB a * rgbaB b) / 255
let alpha = (rgbaA a * rgbaA b) / 255
rgba red green blue alpha
```
Note that haku does not clamp colors to the `0` to `1` range.
@ -339,33 +339,18 @@ Before scribbles are drawn to the wall, colors are converted to 8-bit integers f
This means some loss of precision will happen, which may cause issues with brushes like this one:
```haku
(stroke
128
(rgba 0 0 0 0.1)
(vec))
stroke 128 #00000004 (vec 0 0)
```
If you try to to use this brush to fill up a single spot with black, you will notice that despite all the math suggesting so, the color will end up gray instead.
## Data structures
```haku
(list
(... *)
list)
```
`list` is used to construct a new list.
Currently, lists do not have any operations defined on them.
However, lists made up solely of scribbles are scribbles themselves, which allows for combining scribbles together.
## Shapes
```haku
(to-shape
(value *)
(() shape))
toShape
value : _
-> () | shape
```
Converts the given value to a shape.
@ -375,26 +360,26 @@ Converts the given value to a shape.
- For anything else, returns `()`.
```haku
(line
(start vec)
(end vec)
shape)
line
start : vec
end : vec
-> shape
```
Creates a line segment shape with the provided `start` and `end` points.
```haku
(rect
(position vec)
(size vec)
shape)
rect
position : vec
size : vec
-> shape
(rect
(x number)
(y number)
(width number)
(height number)
shape)
rect
x : number
y : number
width : number
height : number
-> shape
```
Creates a rectangle shape with its top-left corner at `position`, with a given `size` stretching from the top-left corner.
@ -402,16 +387,16 @@ Creates a rectangle shape with its top-left corner at `position`, with a given `
The alternative 4-argument version takes in the rectangle's X/Y coordinates, width, and height as separate arguments instead of aggregating them into a `vec`.
```haku
(circle
(center vec)
(radius number)
shape)
circle
center : vec
radius : number
-> shape
(circle
(x number)
(y number)
(radius number)
shape)
circle
x : number
y : number
radius : number
-> shape
```
Creates a circle shape, with its center at `center`, with the provided radius.
@ -421,11 +406,11 @@ The alternative 3-argument version takes in the circle's center X/Y coordinates
## Scribbles
```haku
(stroke
(thickness number)
(color rgba)
(shape shape-like)
scribble)
stroke
thickness : number
color : rgba
shape : shapeLike
-> scribble
```
Creates a stroke scribble, which outlines the provided shape with a stroke of the given thickness and color.
@ -433,10 +418,10 @@ Creates a stroke scribble, which outlines the provided shape with a stroke of th
Point shapes are drawn as squares, and `line` shapes have square caps at the line's endpoints.
```haku
(fill
(color rgba)
(shape shape-like)
scribble)
fill
color : rgba
shape : shapeLike
-> scribble
```
Creates a fill scribble, which fills in the entire area of the provided shape with a solid color.

View file

@ -3,7 +3,7 @@ const defaultBrush = `
-- Try playing around with the numbers,
-- and see what happens!
stroke 8 (rgba 0 0 0 1) (vec 0 0)
stroke 8 #000 (vec 0 0)
`.trim();
export class BrushEditor extends HTMLElement {