From 29a80854a4bd117b92897fe5b306964a8a71ccb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Tue, 2 Sep 2025 18:44:38 +0200 Subject: [PATCH 01/13] fix parsing prefix operators in calls --- crates/haku-wasm/src/lib.rs | 31 +++++++-------- crates/haku/src/parser.rs | 75 ++++++++++++++++++++++++------------- crates/haku/src/token.rs | 4 ++ static/brush-box.js | 2 +- 4 files changed, 69 insertions(+), 43 deletions(-) diff --git a/crates/haku-wasm/src/lib.rs b/crates/haku-wasm/src/lib.rs index 8459dc6..85c0635 100644 --- a/crates/haku-wasm/src/lib.rs +++ b/crates/haku-wasm/src/lib.rs @@ -401,6 +401,7 @@ unsafe extern "C" fn haku_compile_brush( "compiling: parsed successfully into {} AST nodes", ast.len() ); + // debug!("ast: {}", ast::dump::dump(&ast, root, Some(code))); let src = Source { code, ast: &ast }; @@ -431,21 +432,21 @@ unsafe extern "C" fn haku_compile_brush( ); debug!("compiling: {closure_spec:?}"); - debug!("bytecode: {:?}", chunk.bytecode); - { - let mut cursor = 0_usize; - for info in &chunk.span_info { - let slice = &chunk.bytecode[cursor..cursor + info.len as usize]; - debug!( - "{:?} | 0x{:x} {:?} | {:?}", - info.span, - cursor, - slice, - info.span.slice(src.code), - ); - cursor += info.len as usize; - } - } + // debug!("bytecode: {:?}", chunk.bytecode); + // { + // let mut cursor = 0_usize; + // for info in &chunk.span_info { + // let slice = &chunk.bytecode[cursor..cursor + info.len as usize]; + // debug!( + // "{:?} | 0x{:x} {:?} | {:?}", + // info.span, + // cursor, + // slice, + // info.span.slice(src.code), + // ); + // cursor += info.len as usize; + // } + // } instance.compile_result2 = Some(CompileResult { defs_string: instance.defs.serialize_defs(), diff --git a/crates/haku/src/parser.rs b/crates/haku/src/parser.rs index a8e0cee..5d02a5a 100644 --- a/crates/haku/src/parser.rs +++ b/crates/haku/src/parser.rs @@ -311,31 +311,13 @@ enum Tighter { fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] - enum Spacing { - Loose, + enum Tightness { + Loose(usize), Call, - Tight, + Tight(usize), } - fn tightness((kind, spaces): (TokenKind, Spaces)) -> Option<(Spacing, usize)> { - let spacing = match kind { - // There are a few types of operators which are independent of tightness. - - // For : and =, it does not matter if they're spelled one way or the other, because - // there is only one way to use them (at the beginning of the expression). - TokenKind::Colon | TokenKind::Equal => Spacing::Loose, - - // For calls, there is a special intermediate level, such that they can sit between - // loose operators and tight operators. - _ if PREFIX_TOKENS.contains(kind) => Spacing::Call, - - // For everything else, the usual rules apply. - _ => match spaces.pair() { - (false, false) => Spacing::Tight, - (true, true) => Spacing::Loose, - _ => return None, // not a valid infix operator - }, - }; + fn tightness((kind, spaces): (TokenKind, Spaces)) -> Option { let index = match kind { TokenKind::Equal | TokenKind::Colon => 0, // 1: reserved for `and` and `or` @@ -350,7 +332,28 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { _ if PREFIX_TOKENS.contains(kind) => 5, _ => return None, // not an infix operator }; - Some((spacing, index)) + Some(match kind { + // There are a few types of operators which are independent of tightness. + + // For : and =, it does not matter if they're spelled one way or the other, because + // there is only one way to use them (at the beginning of the expression). + TokenKind::Colon | TokenKind::Equal => Tightness::Loose(index), + + // For unary -, we treat it as having Tight spacing rather than Call, else it would + // be allowed to begin function calls. + TokenKind::Minus if spaces.pair() == (true, false) => Tightness::Tight(index), + + // For calls, there is a special intermediate level, such that they can sit between + // loose operators and tight operators. + _ if PREFIX_TOKENS.contains(kind) => Tightness::Call, + + // For everything else, the usual rules apply. + _ => match spaces.pair() { + (false, false) => Tightness::Tight(index), + (true, true) => Tightness::Loose(index), + _ => return None, // not a valid infix operator + }, + }) } let Some(right_tightness) = tightness(right) else { @@ -361,6 +364,12 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { return Tighter::Right; }; + // When we're inside a call, subsequent arguments must not be slurped into the current + // expression, as it would result in calls being parsed as (vec (1 (-1))), which is not correct. + if left_tightness == Tightness::Call { + return Tighter::Left; + } + if right_tightness > left_tightness { Tighter::Right } else { @@ -585,18 +594,29 @@ const PREFIX_TOKENS: TokenKindSet = TokenKindSet::new(&[ ]); fn prefix(p: &mut Parser) -> Closed { - match p.peek() { + let (kind, spaces) = p.peek_with_spaces(); + match kind { TokenKind::Ident => one(p, NodeKind::Ident), TokenKind::Tag => one(p, NodeKind::Tag), TokenKind::Number => one(p, NodeKind::Number), TokenKind::Color => one(p, NodeKind::Color), TokenKind::LBrack => list(p), - TokenKind::Minus | TokenKind::Not => unary(p), + TokenKind::Minus if !spaces.right() => unary(p), + TokenKind::Not => unary(p), TokenKind::LParen => paren(p), TokenKind::Backslash => lambda(p), TokenKind::If => if_expr(p), + TokenKind::Minus if spaces.pair() == (false, true) => { + let span = p.span(); + p.emit(Diagnostic::error( + span, + "`-` operator must be surrounded by an equal amount of spaces", + )); + p.advance_with_error() + } + _ => { assert!( !PREFIX_TOKENS.contains(p.peek()), @@ -615,9 +635,9 @@ fn prefix(p: &mut Parser) -> Closed { } fn infix(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { - match op.0 { + let (kind, spaces) = op; + match kind { TokenKind::Plus - | TokenKind::Minus | TokenKind::Star | TokenKind::Slash | TokenKind::EqualEqual @@ -627,6 +647,7 @@ fn infix(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { | TokenKind::Greater | TokenKind::GreaterEqual | TokenKind::Colon => infix_binary(p, op), + TokenKind::Minus if spaces.are_balanced() => infix_binary(p, op), TokenKind::Equal => infix_let(p, op), diff --git a/crates/haku/src/token.rs b/crates/haku/src/token.rs index c613408..047caf0 100644 --- a/crates/haku/src/token.rs +++ b/crates/haku/src/token.rs @@ -135,6 +135,10 @@ impl Spaces { pub fn pair(self) -> (bool, bool) { (self.left(), self.right()) } + + pub fn are_balanced(self) -> bool { + matches!(self.pair(), (true, true) | (false, false)) + } } impl fmt::Debug for Spaces { diff --git a/static/brush-box.js b/static/brush-box.js index dfcc850..27750af 100644 --- a/static/brush-box.js +++ b/static/brush-box.js @@ -100,7 +100,7 @@ norm: \\u -> u / vec l l perpClockwise: \\v -> - vec (vecY v) (-(vecX v)) + vec (vecY v) -(vecX v) withDotter \\d -> pi = 3.14159265 From 9808d3227f5831fe2898f3a40ea0c6aae175a429 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Tue, 2 Sep 2025 20:02:48 +0200 Subject: [PATCH 02/13] fix a few bugs with the new precedence rules --- crates/haku-wasm/src/lib.rs | 33 +++++------ crates/haku/src/parser.rs | 28 +++++---- docs/rkgk.dj | 111 +++++++++++++++++++++++++----------- 3 files changed, 113 insertions(+), 59 deletions(-) diff --git a/crates/haku-wasm/src/lib.rs b/crates/haku-wasm/src/lib.rs index 85c0635..5030c58 100644 --- a/crates/haku-wasm/src/lib.rs +++ b/crates/haku-wasm/src/lib.rs @@ -432,22 +432,23 @@ unsafe extern "C" fn haku_compile_brush( ); debug!("compiling: {closure_spec:?}"); - // debug!("bytecode: {:?}", chunk.bytecode); - // { - // let mut cursor = 0_usize; - // for info in &chunk.span_info { - // let slice = &chunk.bytecode[cursor..cursor + info.len as usize]; - // debug!( - // "{:?} | 0x{:x} {:?} | {:?}", - // info.span, - // cursor, - // slice, - // info.span.slice(src.code), - // ); - // cursor += info.len as usize; - // } - // } - + /* + debug!("bytecode: {:?}", chunk.bytecode); + { + let mut cursor = 0_usize; + for info in &chunk.span_info { + let slice = &chunk.bytecode[cursor..cursor + info.len as usize]; + debug!( + "{:?} | 0x{:x} {:?} | {:?}", + info.span, + cursor, + slice, + info.span.slice(src.code), + ); + cursor += info.len as usize; + } + } + // */ instance.compile_result2 = Some(CompileResult { defs_string: instance.defs.serialize_defs(), tags_string: instance.defs.serialize_tags(), diff --git a/crates/haku/src/parser.rs b/crates/haku/src/parser.rs index 5d02a5a..3a99adf 100644 --- a/crates/haku/src/parser.rs +++ b/crates/haku/src/parser.rs @@ -329,7 +329,7 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { | TokenKind::GreaterEqual => 2, TokenKind::Plus | TokenKind::Minus | TokenKind::Star | TokenKind::Slash => 3, // 4: reserve for `.` - _ if PREFIX_TOKENS.contains(kind) => 5, + _ if is_prefix_token((kind, spaces)) => 5, _ => return None, // not an infix operator }; Some(match kind { @@ -341,11 +341,11 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { // For unary -, we treat it as having Tight spacing rather than Call, else it would // be allowed to begin function calls. - TokenKind::Minus if spaces.pair() == (true, false) => Tightness::Tight(index), + TokenKind::Minus if !spaces.are_balanced() => Tightness::Tight(index), // For calls, there is a special intermediate level, such that they can sit between // loose operators and tight operators. - _ if PREFIX_TOKENS.contains(kind) => Tightness::Call, + _ if is_prefix_token((kind, spaces)) => Tightness::Call, // For everything else, the usual rules apply. _ => match spaces.pair() { @@ -366,7 +366,7 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { // When we're inside a call, subsequent arguments must not be slurped into the current // expression, as it would result in calls being parsed as (vec (1 (-1))), which is not correct. - if left_tightness == Tightness::Call { + if left_tightness == Tightness::Call && right.0 == TokenKind::Minus && !right.1.are_balanced() { return Tighter::Left; } @@ -579,13 +579,15 @@ fn if_expr(p: &mut Parser) -> Closed { p.close(o, NodeKind::If) } +// TODO: There is a lot of special casing around `-` being both a prefix and an infix token. +// Maybe there's a way to simplify it? + // NOTE: This must be synchronised with the match expression in prefix(). const PREFIX_TOKENS: TokenKindSet = TokenKindSet::new(&[ TokenKind::Ident, TokenKind::Tag, TokenKind::Number, TokenKind::Color, - TokenKind::Minus, TokenKind::Not, TokenKind::LParen, TokenKind::Backslash, @@ -593,6 +595,10 @@ const PREFIX_TOKENS: TokenKindSet = TokenKindSet::new(&[ TokenKind::LBrack, ]); +fn is_prefix_token((kind, spaces): (TokenKind, Spaces)) -> bool { + PREFIX_TOKENS.contains(kind) || (kind == TokenKind::Minus && !spaces.are_balanced()) +} + fn prefix(p: &mut Parser) -> Closed { let (kind, spaces) = p.peek_with_spaces(); match kind { @@ -602,7 +608,7 @@ fn prefix(p: &mut Parser) -> Closed { TokenKind::Color => one(p, NodeKind::Color), TokenKind::LBrack => list(p), - TokenKind::Minus if !spaces.right() => unary(p), + TokenKind::Minus if spaces.pair() == (true, false) => unary(p), TokenKind::Not => unary(p), TokenKind::LParen => paren(p), TokenKind::Backslash => lambda(p), @@ -619,9 +625,9 @@ fn prefix(p: &mut Parser) -> Closed { _ => { assert!( - !PREFIX_TOKENS.contains(p.peek()), - "{:?} found in PREFIX_TOKENS", - p.peek() + !is_prefix_token(p.peek_with_spaces()), + "{:?} is not a prefix token", + p.peek_with_spaces() ); let span = p.span(); @@ -651,7 +657,7 @@ fn infix(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { TokenKind::Equal => infix_let(p, op), - _ if PREFIX_TOKENS.contains(op.0) => infix_call(p, op), + _ if is_prefix_token(op) => infix_call(p, op), _ => panic!("unhandled infix operator {op:?}"), } @@ -671,7 +677,7 @@ fn infix_binary(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { } fn infix_call(p: &mut Parser, mut arg: (TokenKind, Spaces)) -> NodeKind { - while PREFIX_TOKENS.contains(p.peek()) { + while is_prefix_token(p.peek_with_spaces()) { precedence_parse(p, arg); arg = p.peek_with_spaces(); } diff --git a/docs/rkgk.dj b/docs/rkgk.dj index 7335554..e91e98c 100644 --- a/docs/rkgk.dj +++ b/docs/rkgk.dj @@ -87,7 +87,7 @@ If you want to draw multiple scribbles, you can wrap them into a list, which we withDotter \d -> [ stroke 8 #F00 (d To + vec 4 0) - stroke 8 #00F (d To + vec (-4) 0) + stroke 8 #00F (d To + vec -4 0) ] ``` @@ -109,25 +109,15 @@ withDotter \d -> [ [ stroke 8 #F00 (d To + vec 4 0) - stroke 8 #00F (d To + vec (-4) 0) + stroke 8 #00F (d To + vec -4 0) ] [ stroke 8 #FF0 (d To + vec 0 4) - stroke 8 #0FF (d To + vec 0 (-4)) + stroke 8 #0FF (d To + vec 0 -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! @@ -186,7 +176,7 @@ haku vectors however are a little more constrained, because they always contain 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. +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. 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)`. @@ -205,7 +195,7 @@ withDotter \d -> stroke 8 #000 (d To + vec 10 0) -- moved 10 pixels rightwards ``` -Also note how the `d To` expression is parenthesized. +Also note how the `d To + vec 10 0` expression is parenthesized. This is because otherwise, its individual parts would be interpreted as separate arguments to `stroke`, which is not what we want! Anyways, with all that, we let haku mix all the ingredients together, and get a black dot under the cursor. @@ -249,16 +239,72 @@ haku also supports other kinds of shapes: circles and rectangles. ```haku withDotter \d -> [ - stroke 8 #F00 (circle (d To + vec (-16) 0) 16) - stroke 8 #00F (rect (d To + vec 0 (-16)) 32 32) + stroke 8 #F00 (circle (d To + vec -16 0) 16) + stroke 8 #00F (rect (d To + vec 0 -16) (vec 32 32)) ] ``` - `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 equal! + +## Math in haku + +While haku is based entirely in pure math, it is important to note that haku is _not_ math notation! +It is a textual programming language, and has different rules concerning order of operations than math. + +::: aside + +If you've programmed in any other language, you might find those rules alien. +But I promise you, they make sense in the context of the rest of the language! + +::: + +In traditional math notation, the conventional order of operations is: + +1. Parentheses +2. Exponentiation +3. Multiplication and division +4. Addition and subtraction + +haku does not have an exponentiation operator. +That purpose is served by the function `pow`. +It does however have parentheses, multiplication, division, addition, and subtraction. + +Unlike in math notation, addition, subtraction, multiplication, and division, are _all_ calculated from left to right---multiplication and division does not take precedence over addition and subtraction. +So for the expression `2 + 2 * 2`, the result is `8`, and not `6`! + +Since this can be inconvenient at times, there is a way to work around that. +haku has a distinction between _tight_ and _loose_ operators, where tight operators always take precedence over loose ones in the order of operations. + +Remove the spaces around the `*` multiplication operator, like `2 + 2*2`, and the result is now `6` again---because we made `*` tight! + +This is convenient when representing fractions. +If you want a constant like half-π, the way to write it is `1/2*pi`---and order of operations will never mess you up, as long as you keep it tight without spaces! + +The same thing happens with functions. +For example, if you wanted to calculate the sine of `1/2*pi*x`, as long as you write that as `sin 1/2*pi*x`, with the whole argument without spaces, you won't have to wrap it in parentheses. + +Inside a single whole tight or loose expression, there is still an order of operations. +In fact, here's the full order of operations in haku for reference: + +1. Tight + + 1. Arithmetic: `+`, `-`, `*`, `/` + 1. Comparisons: `==`, `!=`, `<`, `<=`, `>`, `>=` + +1. Function calls +1. Loose + + 1. Arithmetic + 1. Comparisons + 1. Variables: `:`, `=` + +Naturally, you can still use parentheses when the loose-tight distinction is not enough. + + ## Programming in haku So far we've been using haku solely to describe data. @@ -270,7 +316,7 @@ Remember that example from before? withDotter \d -> [ stroke 8 #F00 (d To + vec 4 0) - stroke 8 #00F (d To + vec (-4) 0) + stroke 8 #00F (d To + vec -4 0) ] ``` @@ -281,7 +327,7 @@ If we wanted to change the size of the points, we'd need to first update the str withDotter \d -> [ stroke 4 #F00 (d To + vec 4 0) - stroke 4 #00F (d To + vec (-4) 0) + stroke 4 #00F (d To + vec -4 0) --- ] ``` @@ -294,8 +340,8 @@ So we also have to update their positions. [ stroke 4 #F00 (d To + vec 2 0) --- - stroke 4 #00F (d To + vec (-2) 0) - -- + stroke 4 #00F (d To + vec -2 0) + -- ] ``` @@ -322,7 +368,7 @@ thickness: 4 withDotter \d -> [ stroke thickness #F00 (d To + vec 2 0) - stroke thickness #00F (d To + vec (-2) 0) + stroke thickness #00F (d To + vec -2 0) --------- ] ``` @@ -355,7 +401,7 @@ xOffset: 2 withDotter \d -> [ stroke thickness #F00 (d To + vec xOffset 0) - stroke thickness #00F (d To + vec (-xOffset) 0) + stroke thickness #00F (d To + vec -xOffset 0) --------- ] ``` @@ -371,7 +417,7 @@ Uppercase names are special values we call _tags_. Tags are values which represent names. For example, the `To` in `d To` is a tag. -It represents the name of the piece of data we're extracting from `d`. +It is the name of the piece of data we're extracting from `d`. There are also two special tags, `True` and `False`, which represent [Boolean](https://en.wikipedia.org/wiki/Boolean_algebra) truth and falsehood. @@ -388,7 +434,7 @@ xOffset: 2 withDotter \d -> [ stroke thickness #F00 (d To + vec xOffset 0) - stroke thickness #00F (d To + vec (-xOffset) 0) + stroke thickness #00F (d To + vec -xOffset 0) ] ``` @@ -402,7 +448,7 @@ xOffset: thickness / 2 withDotter \d -> [ stroke thickness #F00 (d To + vec xOffset 0) - stroke thickness #00F (d To + vec (-xOffset) 0) + stroke thickness #00F (d To + vec -xOffset 0) ] ``` @@ -564,6 +610,7 @@ Seriously, 64 is my limit. I wonder if there's any way we could automate this? + ### The Ouroboros You know the drill by now. @@ -587,7 +634,7 @@ splat: \d, radius -> airbrush: \d, size -> [ splat d size - airbrush d (size - 8) + airbrush d size-8 ] withDotter \d -> @@ -649,7 +696,7 @@ airbrush: \d, size -> if (size > 0) [ splat d size - airbrush d (size - 8) + airbrush d size-8 ] else [] @@ -675,8 +722,8 @@ airbrush: \d, size -> if (size > 0) [ splat d size - airbrush d (size - 1) - --- + airbrush d size-1 + --- ] else [] @@ -696,7 +743,7 @@ airbrush: \d, size -> if (size > 0) [ splat d size - airbrush d (size - 1) + airbrush d size-1 ] else [] From 449f2b59dfe62fc70f4f96d2f01553e833832f06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Tue, 2 Sep 2025 20:02:48 +0200 Subject: [PATCH 03/13] disable ligatures in code it can be hard for first-time users to understand what these ligatures mean or how to type them --- static/base.css | 1 + 1 file changed, 1 insertion(+) diff --git a/static/base.css b/static/base.css index 6fc9acd..bf170d6 100644 --- a/static/base.css +++ b/static/base.css @@ -53,6 +53,7 @@ code, textarea { font-family: var(--font-monospace); line-height: var(--line-height); + font-variant-ligatures: none; } /* Buttons */ From 45099916fe6b4fc7211dc081ff3d16adf8ea4121 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Tue, 2 Sep 2025 20:15:35 +0200 Subject: [PATCH 04/13] update Zig code to 0.15.0 --- crates/haku2/build.rs | 1 + crates/haku2/src/haku2.zig | 3 ++- crates/haku2/src/value.zig | 24 ++++++++++++------------ crates/haku2/src/vm.zig | 4 ++-- 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/crates/haku2/build.rs b/crates/haku2/build.rs index a68dc93..3d49a8b 100644 --- a/crates/haku2/build.rs +++ b/crates/haku2/build.rs @@ -46,6 +46,7 @@ fn main() -> Result<(), Box> { .arg("--prominent-compile-errors") .arg("--color") .arg(color) + .arg("-freference-trace") // Build output .arg("--cache-dir") .arg(out_path.join("zig-cache")) diff --git a/crates/haku2/src/haku2.zig b/crates/haku2/src/haku2.zig index ec428fc..cfadabe 100644 --- a/crates/haku2/src/haku2.zig +++ b/crates/haku2/src/haku2.zig @@ -22,8 +22,9 @@ pub const std_options: std.Options = .{ }; pub fn enableLogScope(scope: @TypeOf(.enum_literal)) bool { + // Replace any of the false returns below to enable log scopes for the build. if (scope == .vm) - return false + return true else return true; } diff --git a/crates/haku2/src/value.zig b/crates/haku2/src/value.zig index d1498ff..e34bc24 100644 --- a/crates/haku2/src/value.zig +++ b/crates/haku2/src/value.zig @@ -69,24 +69,24 @@ pub const Value = union(enum) { }; } - pub fn format(value: Value, comptime fmt: []const u8, options: std.fmt.FormatOptions, writer: anytype) !void { + pub fn format(value: Value, writer: *std.Io.Writer) !void { switch (value) { - .nil => try std.fmt.formatBuf("Nil", options, writer), - .false => try std.fmt.formatBuf("False", options, writer), - .true => try std.fmt.formatBuf("True", options, writer), - inline .tag, .number => |x| try std.fmt.format(writer, "{d}", .{x}), - inline .vec4, .rgba => |x| try std.fmt.format(writer, "{s}{d}", .{ @tagName(value), x }), + .nil => try writer.writeAll("Nil"), + .false => try writer.writeAll("False"), + .true => try writer.writeAll("True"), + inline .tag, .number => |x| try writer.print("{d}", .{x}), + inline .vec4, .rgba => |x| try writer.print("{s}{d}", .{ @tagName(value), x }), .ref => |ref| switch (ref.*) { - .closure => |c| try std.fmt.format(writer, "function {s}", .{c.name}), + .closure => |c| try writer.print("function {s}", .{c.name}), .list => |l| { - try std.fmt.formatBuf("[", options, writer); + try writer.writeAll("["); for (l, 0..) |elem, i| { - if (i != 0) try std.fmt.formatBuf(", ", options, writer); - try elem.format(fmt, options, writer); + if (i != 0) try writer.writeAll(", "); + try elem.format(writer); } - try std.fmt.formatBuf("]", options, writer); + try writer.writeAll("]"); }, - inline .shape, .scribble, .reticle => |x| try std.fmt.format(writer, "{}", .{x}), + inline .shape, .scribble, .reticle => |x| try writer.print("{}", .{x}), }, } } diff --git a/crates/haku2/src/vm.zig b/crates/haku2/src/vm.zig index 9f90953..910cb13 100644 --- a/crates/haku2/src/vm.zig +++ b/crates/haku2/src/vm.zig @@ -167,7 +167,7 @@ pub fn push(vm: *Vm, val: Value) Error!void { if (vm.stack_top >= vm.stack.len) { return vm.throw("too many live temporary values (local variables and expression operands)", .{}); } - log.debug("PUSH {any} <- {}", .{ vm.stack[0..vm.stack_top], val }); + log.debug("PUSH {any} <- {f}", .{ vm.stack[0..vm.stack_top], val }); vm.stack[vm.stack_top] = val; vm.stack_top += 1; } @@ -176,7 +176,7 @@ pub fn pop(vm: *Vm) Error!Value { try vm.validateBytecode(vm.stack_top > 0, "value stack underflow", .{}); vm.stack_top -= 1; const result = vm.stack[vm.stack_top]; - log.debug("POP {any} -> {}", .{ vm.stack[0..vm.stack_top], result }); + log.debug("POP {any} -> {f}", .{ vm.stack[0..vm.stack_top], result }); return vm.stack[vm.stack_top]; } From 2eea1f201f4ff7829b8b17b192d3a2aeec7c9e9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Tue, 2 Sep 2025 20:30:47 +0200 Subject: [PATCH 05/13] h2: add better VM value stack tracing --- crates/haku2/src/vm.zig | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/crates/haku2/src/vm.zig b/crates/haku2/src/vm.zig index 910cb13..7bec9a1 100644 --- a/crates/haku2/src/vm.zig +++ b/crates/haku2/src/vm.zig @@ -147,6 +147,24 @@ pub fn stackFrame(vm: *const Vm, index: usize) StackFrame { } } +const StackFmt = struct { + stack: []const Value, + + pub fn format(f: *const StackFmt, writer: *std.Io.Writer) !void { + try writer.writeAll("["); + for (f.stack, 0..) |val, i| { + if (i != 0) + try writer.writeAll(", "); + try val.format(writer); + } + try writer.writeAll("]"); + } +}; + +fn stackFmt(stack: []const Value) StackFmt { + return .{ .stack = stack }; +} + /// Debug assertion for bytecode validity. /// In future versions, this may become disabled in release builds. fn validateBytecode(vm: *Vm, ok: bool, comptime fmt: []const u8, args: anytype) Error!void { @@ -167,7 +185,7 @@ pub fn push(vm: *Vm, val: Value) Error!void { if (vm.stack_top >= vm.stack.len) { return vm.throw("too many live temporary values (local variables and expression operands)", .{}); } - log.debug("PUSH {any} <- {f}", .{ vm.stack[0..vm.stack_top], val }); + log.debug("PUSH {f} <- {f}", .{ stackFmt(vm.stack[0..vm.stack_top]), val }); vm.stack[vm.stack_top] = val; vm.stack_top += 1; } @@ -176,7 +194,7 @@ pub fn pop(vm: *Vm) Error!Value { try vm.validateBytecode(vm.stack_top > 0, "value stack underflow", .{}); vm.stack_top -= 1; const result = vm.stack[vm.stack_top]; - log.debug("POP {any} -> {f}", .{ vm.stack[0..vm.stack_top], result }); + log.debug("POP {f} -> {f}", .{ stackFmt(vm.stack[0..vm.stack_top]), result }); return vm.stack[vm.stack_top]; } From 958201cd18809447a5cb82931adc7f7d69626704 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Tue, 2 Sep 2025 20:30:47 +0200 Subject: [PATCH 06/13] h2: add better comment about how to use root.enableLogScope --- crates/haku2/src/haku2.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/haku2/src/haku2.zig b/crates/haku2/src/haku2.zig index cfadabe..136f21f 100644 --- a/crates/haku2/src/haku2.zig +++ b/crates/haku2/src/haku2.zig @@ -24,7 +24,7 @@ pub const std_options: std.Options = .{ pub fn enableLogScope(scope: @TypeOf(.enum_literal)) bool { // Replace any of the false returns below to enable log scopes for the build. if (scope == .vm) - return true + return false else return true; } From c4ad609717b734e756359b705055635b6bb63768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Tue, 2 Sep 2025 20:30:47 +0200 Subject: [PATCH 07/13] h2: fix implementation of Value.gt not actually being a greater-than operation how did I miss that (a < b) is the same as (b > a) is beyond me --- crates/haku2/src/value.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/haku2/src/value.zig b/crates/haku2/src/value.zig index e34bc24..3269a60 100644 --- a/crates/haku2/src/value.zig +++ b/crates/haku2/src/value.zig @@ -48,7 +48,7 @@ pub const Value = union(enum) { } pub fn gt(a: Value, b: Value) ?bool { - return !a.eql(b) and !(a.lt(b) orelse return null); + return b.lt(a); } pub fn typeName(value: Value) []const u8 { From e31dde10487951c2767ca5d4c9e7bbecc188c9b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Tue, 2 Sep 2025 20:30:47 +0200 Subject: [PATCH 08/13] add .ignore file --- .ignore | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .ignore diff --git a/.ignore b/.ignore new file mode 100644 index 0000000..be8298c --- /dev/null +++ b/.ignore @@ -0,0 +1,3 @@ +*.ttf +*.woff2 +*.png From 69cc34d07e2837eb473c49085c08ea905f7752b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Tue, 2 Sep 2025 20:30:47 +0200 Subject: [PATCH 09/13] h1: make ' and ? only allowed as suffixes in identifiers --- crates/haku/src/lexer.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/haku/src/lexer.rs b/crates/haku/src/lexer.rs index f3e4ce2..3ebff7f 100644 --- a/crates/haku/src/lexer.rs +++ b/crates/haku/src/lexer.rs @@ -57,7 +57,11 @@ fn one_or_two(l: &mut Lexer<'_>, kind1: TokenKind, c2: char, kind2: TokenKind) - } fn is_ident_char(c: char) -> bool { - matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '\'' | '?') + matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '_') +} + +fn is_ident_extra_char(c: char) -> bool { + matches!(c, '\'' | '?') } fn ident(l: &mut Lexer<'_>) -> TokenKind { @@ -65,6 +69,9 @@ fn ident(l: &mut Lexer<'_>) -> TokenKind { while is_ident_char(l.current()) { l.advance(); } + while is_ident_extra_char(l.current()) { + l.advance(); + } let end = l.position; match Span::new(start, end).slice(l.input) { From ec7ee9626fc394661074aa2ecb0625c72a048ad4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Tue, 2 Sep 2025 22:03:42 +0200 Subject: [PATCH 10/13] implement `.` and `|` operators `f . x` is the same as `f x`, and is mostly useful when used as an argument to another function call. this allows us to simulate record field syntax very well: `d.Num` is the same as `d Num`, but with high precedence `a |f b` is the same as `f a b` and effectively allows you to turn any function into an arithmetic operator. it's the main reason behind + - * / having the same precedence---they now chain very nicely with pipes closes #126 (`|` operator) closes #127 (`.` operator) --- crates/haku/src/compiler.rs | 150 ++++++++++++++++++++++++++---------- crates/haku/src/lexer.rs | 2 + crates/haku/src/parser.rs | 21 +++-- crates/haku/src/token.rs | 2 + 4 files changed, 125 insertions(+), 50 deletions(-) diff --git a/crates/haku/src/compiler.rs b/crates/haku/src/compiler.rs index a458c5e..c60fb11 100644 --- a/crates/haku/src/compiler.rs +++ b/crates/haku/src/compiler.rs @@ -361,52 +361,43 @@ fn compile_binary<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) - } let name = src.ast.span(op).slice(src.code); - if name == "=" { - c.emit(Diagnostic::error( - src.ast.span(op), - "defs `a = b` may only appear at the top level", - )); - return Ok(()); + match name { + ":" => { + // Invalid use of a def inside an expression. + c.emit(Diagnostic::error( + src.ast.span(op), + "defs `a: b` may only appear at the top level", + )); + Ok(()) + } + "." => compile_dot_call(c, src, left, right), + "|" => compile_pipe_call(c, src, left, right), + _ => { + compile_expr(c, src, left)?; + compile_expr(c, src, right)?; + if let Some(index) = system::resolve(SystemFnArity::Binary, name) { + let argument_count = 2; + c.chunk.emit_opcode(Opcode::System)?; + c.chunk.emit_u8(index)?; + c.chunk.emit_u8(argument_count)?; + } else { + c.emit(Diagnostic::error( + src.ast.span(op), + "this binary operator is currently unimplemented", + )); + } + Ok(()) + } } - - compile_expr(c, src, left)?; - compile_expr(c, src, right)?; - if let Some(index) = system::resolve(SystemFnArity::Binary, name) { - let argument_count = 2; - c.chunk.emit_opcode(Opcode::System)?; - c.chunk.emit_u8(index)?; - c.chunk.emit_u8(argument_count)?; - } else { - c.emit(Diagnostic::error( - src.ast.span(op), - "this unary operator is currently unimplemented", - )); - } - - Ok(()) } -fn compile_call<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> CompileResult { - let mut walk = src.ast.walk(node_id); - let Some(func) = walk.node() else { - return Ok(()); - }; +fn emit_nary_call<'a>( + c: &mut Compiler<'a>, + src: &Source<'a>, + func: NodeId, + argument_count: u8, +) -> CompileResult { let name = src.ast.span(func).slice(src.code); - - let mut argument_count = 0; - while let Some(arg) = walk.node() { - compile_expr(c, src, arg)?; - argument_count += 1; - } - - let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| { - c.emit(Diagnostic::error( - src.ast.span(node_id), - "function call has too many arguments", - )); - 0 - }); - if let (NodeKind::Ident, Some(index)) = ( src.ast.kind(func), system::resolve(SystemFnArity::Nary, name), @@ -423,6 +414,81 @@ fn compile_call<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> c.chunk.emit_opcode(Opcode::Call)?; c.chunk.emit_u8(argument_count)?; } + Ok(()) +} + +fn compile_call<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -> CompileResult { + let mut walk = src.ast.walk(node_id); + let Some(func) = walk.node() else { + return Ok(()); + }; + + let mut argument_count = 0; + while let Some(arg) = walk.node() { + compile_expr(c, src, arg)?; + argument_count += 1; + } + + let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| { + c.emit(Diagnostic::error( + src.ast.span(node_id), + "function call has too many arguments", + )); + 0 + }); + + emit_nary_call(c, src, func, argument_count)?; + + Ok(()) +} + +fn compile_dot_call<'a>( + c: &mut Compiler<'a>, + src: &Source<'a>, + func: NodeId, + right: NodeId, +) -> CompileResult { + compile_expr(c, src, right)?; + emit_nary_call(c, src, func, 1)?; + + Ok(()) +} + +fn compile_pipe_call<'a>( + c: &mut Compiler<'a>, + src: &Source<'a>, + left: NodeId, + call: NodeId, +) -> CompileResult { + if src.ast.kind(call) != NodeKind::Call { + c.emit(Diagnostic::error( + src.ast.span(call), + "the right side of a pipe `|` must be a function call", + )); + return Ok(()); + } + + let mut walk = src.ast.walk(call); + let Some(func) = walk.node() else { + return Ok(()); + }; + + compile_expr(c, src, left)?; + let mut argument_count = 1; + while let Some(arg) = walk.node() { + compile_expr(c, src, arg)?; + argument_count += 1; + } + + let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| { + c.emit(Diagnostic::error( + src.ast.span(call), + "function call has too many arguments", + )); + 0 + }); + + emit_nary_call(c, src, func, argument_count)?; Ok(()) } diff --git a/crates/haku/src/lexer.rs b/crates/haku/src/lexer.rs index 3ebff7f..781d7d4 100644 --- a/crates/haku/src/lexer.rs +++ b/crates/haku/src/lexer.rs @@ -215,6 +215,8 @@ fn token(l: &mut Lexer<'_>) -> (TokenKind, Span, bool) { '!' => one_or_two(l, TokenKind::Not, '=', TokenKind::NotEqual), '<' => one_or_two(l, TokenKind::Less, '=', TokenKind::LessEqual), '>' => one_or_two(l, TokenKind::Greater, '=', TokenKind::GreaterEqual), + '.' => one(l, TokenKind::Dot), + '|' => one(l, TokenKind::Pipe), '\n' => return newline(l, has_left_space), '(' => one(l, TokenKind::LParen), diff --git a/crates/haku/src/parser.rs b/crates/haku/src/parser.rs index 3a99adf..fa1af6d 100644 --- a/crates/haku/src/parser.rs +++ b/crates/haku/src/parser.rs @@ -327,17 +327,20 @@ fn tighter(left: (TokenKind, Spaces), right: (TokenKind, Spaces)) -> Tighter { | TokenKind::LessEqual | TokenKind::Greater | TokenKind::GreaterEqual => 2, - TokenKind::Plus | TokenKind::Minus | TokenKind::Star | TokenKind::Slash => 3, - // 4: reserve for `.` + + TokenKind::Plus + | TokenKind::Minus + | TokenKind::Star + | TokenKind::Slash + | TokenKind::Pipe => 3, + + TokenKind::Dot => 4, _ if is_prefix_token((kind, spaces)) => 5, _ => return None, // not an infix operator }; Some(match kind { - // There are a few types of operators which are independent of tightness. - - // For : and =, it does not matter if they're spelled one way or the other, because - // there is only one way to use them (at the beginning of the expression). - TokenKind::Colon | TokenKind::Equal => Tightness::Loose(index), + // There are a few types of operators which work independent of tightness. + TokenKind::Colon | TokenKind::Equal | TokenKind::Pipe => Tightness::Loose(index), // For unary -, we treat it as having Tight spacing rather than Call, else it would // be allowed to begin function calls. @@ -652,7 +655,9 @@ fn infix(p: &mut Parser, op: (TokenKind, Spaces)) -> NodeKind { | TokenKind::LessEqual | TokenKind::Greater | TokenKind::GreaterEqual - | TokenKind::Colon => infix_binary(p, op), + | TokenKind::Colon + | TokenKind::Dot + | TokenKind::Pipe => infix_binary(p, op), TokenKind::Minus if spaces.are_balanced() => infix_binary(p, op), TokenKind::Equal => infix_let(p, op), diff --git a/crates/haku/src/token.rs b/crates/haku/src/token.rs index 047caf0..96c8282 100644 --- a/crates/haku/src/token.rs +++ b/crates/haku/src/token.rs @@ -28,6 +28,8 @@ pub enum TokenKind { Greater, GreaterEqual, Not, + Dot, + Pipe, // Punctuation Newline, From a4c18c37dc1abb1228251741edc37cfd9735f3bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Wed, 3 Sep 2025 16:57:10 +0200 Subject: [PATCH 11/13] h1: make `|` operator work with anything other than syntactic calls (in which case it just calls the value) in case you call a function to get a function, parenthesise it 1 | getFunction () -- getFunction 1 () 1 | (getFunction ()) -- (getFunction ()) 1 --- crates/haku/src/compiler.rs | 56 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/crates/haku/src/compiler.rs b/crates/haku/src/compiler.rs index c60fb11..f3f8f3d 100644 --- a/crates/haku/src/compiler.rs +++ b/crates/haku/src/compiler.rs @@ -460,36 +460,36 @@ fn compile_pipe_call<'a>( left: NodeId, call: NodeId, ) -> CompileResult { - if src.ast.kind(call) != NodeKind::Call { - c.emit(Diagnostic::error( - src.ast.span(call), - "the right side of a pipe `|` must be a function call", - )); - return Ok(()); + match src.ast.kind(call) { + NodeKind::Call => { + let mut walk = src.ast.walk(call); + let Some(func) = walk.node() else { + return Ok(()); + }; + + compile_expr(c, src, left)?; + let mut argument_count = 1; + while let Some(arg) = walk.node() { + compile_expr(c, src, arg)?; + argument_count += 1; + } + + let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| { + c.emit(Diagnostic::error( + src.ast.span(call), + "function call has too many arguments", + )); + 0 + }); + + emit_nary_call(c, src, func, argument_count)?; + } + _ => { + compile_expr(c, src, left)?; + emit_nary_call(c, src, call, 1)?; + } } - let mut walk = src.ast.walk(call); - let Some(func) = walk.node() else { - return Ok(()); - }; - - compile_expr(c, src, left)?; - let mut argument_count = 1; - while let Some(arg) = walk.node() { - compile_expr(c, src, arg)?; - argument_count += 1; - } - - let argument_count = u8::try_from(argument_count).unwrap_or_else(|_| { - c.emit(Diagnostic::error( - src.ast.span(call), - "function call has too many arguments", - )); - 0 - }); - - emit_nary_call(c, src, func, argument_count)?; - Ok(()) } From b5cdfdb1b6a79d01f8eceb3b03a42f1048829afd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Wed, 3 Sep 2025 16:57:10 +0200 Subject: [PATCH 12/13] introduce haku reference docs --- docs/haku.dj | 520 +++++++++++++++++++++++++++++++++++++++++++++++++++ rkgk.toml | 1 + 2 files changed, 521 insertions(+) create mode 100644 docs/haku.dj diff --git a/docs/haku.dj b/docs/haku.dj new file mode 100644 index 0000000..aa93e43 --- /dev/null +++ b/docs/haku.dj @@ -0,0 +1,520 @@ +# haku reference manual + +haku is a dynamically typed, pure functional programming language, used for programming brushes in rakugaki. + +For the layperson, it can be thought of a beefed up calculator. +It has roots in ordinary school algebra, but has a much more powerful set of features, with the special ability of being able to edit images. + + +## Overview + +haku programs are called _brushes_. +The purpose of a brush is to produce instructions for rakugaki on how the user may interact with the wall (via _reticles_), and what things should be drawn on the wall as a result of those interactions (via _scribbles_). +Collectively, reticles and scribbles are what we call _effects_. + +A brush's task is to compute (or in simpler terms, calculate) an effect that rakugaki will then _perform_. + +In case of reticles, rakugaki will allow the user to interact with the wall, and then ask haku for another effect to perform afterwards (a _continuation_). + +In case of scribbles, rakugaki will draw the scribble onto the wall, without asking haku for more. + +Once rakugaki runs through all effects, we say that the brush has _finished executing_. + + +## Lexical elements + + +### Comments + +haku's most basic lexical element is the _comment_. + +``` +-- This is a comment. +-- こんにちは! +``` + +Comments introduce human-readable remarks into a brush's code. +Their only purpose is documentation. +They serve no semantic meaning, and thus do not affect the result of the brush whatsoever. + +Comments begin with `--`, and span until the end of the current line of text. +Once the line ends, the comment does, too. + +Comments do not necessarily have to appear at the beginning of a line. + +```haku +magnitude: \v -> -- calculate the magnitude of a vector + hypot vecX.v vecY.v +``` + + +### Literals + +Literals represent _literal values_ that are input into the brush. + +haku has a few types of literals, but not all literals are purely lexical elements (some of them can nest)---which is why the different types of literals are covered under the [Expressions](#Expressions) section. + + +### Identifiers + +Identifiers are used for naming values inside a brush. +New names are introduced using [defs](#Structure-of-a-brush) and [lets](#Let-expression). +Once a name is introduced, it may be referenced using its identifier in its corresponding scope---the whole brush for defs, and the following expression in lets. + +An identifier starts with a *lowercase* ASCII letter---`a`--`z` or an underscore---`_`, and is followed by zero or more ASCII letters of any case---`a`--`z`, `A`--`Z`, digits---`0`--`9`, and underscores---`_`. +This then may be followed by an arbitrary number of _suffix characters_ prime symbols---`'` and question marks---`?`. + +By convention, prime symbols are used in the same way they are used in math notation---for introducing a distinct variable of the same name as another variable, usually derived from the previous. +For example, given a variable named `position`, an _updated_ position may be named `position'`. + +The question mark suffix is conventionally used for [boolean](#Boolean-type) variables, as well as boolean-returning functions. +By convention, only one question mark is always used. + +Identifiers starting with *uppercase* ASCII letters---`A`--`Z` are not identifiers, but rather [tags](#tag-literal), and therefore cannot be used as def and let names. + +The following identifiers are reserved as _keywords_, and have special meaning assigned within the language syntax. + +- `if` --- Introduces an [`if` expression](#if-expression). +- `else` --- Introduces the `else` clause in an [`if` expression](#if-expression). + +Additionally, the following identifiers are reserved for future use, and may not be used for naming defs and lets. + +- `and` +- `or` + +By convention, a prime symbol `'` suffix can be used to work around this restriction. +For example, instead of naming a variable `if`, try naming it `if'` (read as _if prime_, "_the other if_"). + + +### Operators and punctuation + +Operators and punctuation share a section in this part of the reference due to their lexical similarity. + +[Operators](#operators) serve as a terse syntax for calling a small set of built-in functions within the program (described in detail in the [system reference documentation](system.html)), while punctuation serves the purpose of syntactic delimiters. + +The following symbols are operators: + +``` ++ - * / +== != < <= > >= ! +``` + +And the following symbols are punctuation: + +``` + +( ) [ ] , += : +. | +\ -> +``` + +`` is literally written down as a line break in programs, which would be invisible in this reference. + + +## Structure of a brush + +A brush is structured like so: + +```haku +def1: expr1 +def2: expr2 +def3: expr3 +-- ... +effectExpr +``` + +That is, there are two parts to a brush: its _defs_, followed by the resulting effect [expression](#Expressions). +The effect produced by this expression is the effect performed by rakugaki when the user interacts with the wall (clicks on it, touches it, starts a pen stroke). + +Defs introduce names for values that are available across the entire program. +They are most commonly used to name constants and functions. + +Example: + +```haku +-- Constant definition +pi: 3.14159265 + +-- Function definition +magnitude: \v -> + hypot vecX.v vecY.v +``` + + +## Expressions + +haku is a strictly expression-oriented language. +There is no idea of statements, as would be the case in lower-level languages like C++ or JavaScript. +This comes from the fact that haku is a _pure_ functional language, which means there aren't any expressions whose result you would want to discard, only for their side effects. + + +### Nil + +An empty parenthesis represents a _nil_ value---that is, literally a value that means "no value." + +```haku +() +``` + +It is one of the only values considered [_false_](#Truth) by the language, other than the [`False` boolean](#Tags). + + +### Numbers + +Numbers in haku are written down as a sequence of ASCII digits---`0`--`9`, followed by an optional decimal point `.` with a decimal part. + +```haku +0 +123 +3.14159265 +``` + +Internally, they are represented by 32-bit floating point numbers. +This means that they have [fairly limited precision](https://float.exposed/0x42f60000), and do not always behave exactly like math on real numbers. +For example, there are magical values for ∞ and -∞ (which _can_ exist and can be operated upon), as well as a value called NaN (not a number), which are produced as results of certain operations that aren't well-defined in math (most commonly division by zero). + + +### Tags + +Tags are similar to [identifiers](#Identifiers), but start with an *uppercase* rather than a lowercase ASCII letter (that is, `A`--`Z`). +They are values which represent _names_. + +This concept may feel a bit alien. +As an example, consider how haku implements record types. +From the perspective of the user, a record type acts like a [function](#Functions) which accepts a tag as an argument---with the tag being the name of the record field. + +```haku +withDotter \d -> + stroke 8 #000 (line d.From d.To) + ---- -- these are tags +``` + +There are also two special tags, `True` and `False`, which are used to represent Boolean logic. + +The boolean `False` is the only [_false_](#Truth) value in the language, other than [nil](#Nil). + + +### Colors + +haku has a literal for representing RGBA colors. +It takes one of the following four forms, from most explicit to least explicit: + +```haku +#RRGGBBAA +#RRGGBB +#RGBA +#RGB +``` + +Each character in a color literal is a hexadecimal digit, with the digits `0`--`9` representing digits 0 to 9, and letters `a`--`f` representing digits 10 to 16 (case insensitive.) + +For `#RGB` and `#RGBA`, the digits are repeated twice implicitly---that is, `#1234` is the same as `#11223344`. + +This syntax is designed to be convenient for working with colors coming from external programs. +For example, you may pick a color from an online palette, and paste its hex code straight into your brush code. + +Example (rakugaki logo colors): + +```haku +white: #FFF +peach: #FFA6A6 +raspberry: #F44096 +``` + + +### Lists + +Lists are fixed-length sequences of values. +They are written down by listing out a sequence of comma `,` or newline-separated items, enclosed in square brackets `[]`. + +```haku +six: [1, 2, 3, 4, 5, 6] +four: [ + 1 + 2 + 3 + 4 +] +``` + +Lists are allowed to nest, as the values may be of any type---including lists themselves. + +Lists are most commonly used to compose scribbles. +A list is also a scribble, which draws the scribbles it contains within itself, from first to last. + + +### Operators + +Operators in haku are used mostly for basic mathematical operations. +They are either unary, written in prefix form `!x`, or binary, written in infix form `a + b`. + +While order of operations in case of unary operators is unambiguous (innermost to outermost), infix operators are not as simple. +Certain infix operators have _precedence_ over others. + +This precedence depends on the operator used, as well as the spacing around it. +Spacing _only_ matters for infix operators; prefix operators may have any amount of spaces around them, though conventionally they are glued to the expression on the right, like `(-1)`, or `vec -1 0`. + +Infix operators with spaces around them are classified as _loose_, and those without spaces around them are _tight_. +An unequal amount of spaces around an infix operator is considered an error (or is parsed as a prefix operator, depending on context). + +Based on these two groups, the precedence of operators is defined as follows: + +1. Prefix operators: `-a`, `!a` +1. Tight + + 1. Dot: `a.b` + 1. Arithmetic: `a+b`, `a-b`, `a*b`, `a/b` + 1. Comparisons: `a==b`, `a!=b`, `ab`, `a>=b` + +1. Function calls: `a b` +1. Loose + + 1. Dot: `a . b` + 1. Arithmetic: `a + b`, `a - b`, `a * b`, `a / b`, `a |b` + 1. Comparisons: `a == b`, `a != b` + 1. Variables: `a: b`, `a = b` + +The operators `+`, `-`, `*`, `/`, `==`, `!=`, `<`, `<=`, `>`, `>=`, `!`, are used for calling functions built into the haku [system library](system.html). + +Other infix tokens listed above have other semantic meaning. + +- `a b`, `.`, and `|` --- Used for calling [functions](#Functions). +- `:` --- Used for introducing [defs](#Structure-of-a-brush). +- `=` --- Used for introducing [lets](#Let-expression). + +Examples of how these precedence rules work in practice: + +```haku +2 + 2 * 2 == 8 -- left to right +2 + 2*2 == 6 -- 2*2 goes first +2+2 * 2 == 8 -- 2+2 goes first +2+2*2 == 8 -- left to right + +sin 2 * pi * x == (sin 2) * pi * x -- function call goes first +sin 2*pi*x == sin (2 * pi * x) -- argument goes first + +-- unintuitive for users of C-likes: +-- prefix `-` has higher precedence than `.` +-vecX.v == (-vecX).v +``` + +One thing that should be noted about haku's operator precedence is that, unlike math notation and most other programming languages, `+`, `-`, `*`, and `/`, are evaluated from left to right. +This is because otherwise they would interact unintuitively with the pipe `|` operator, which is effectively used as an operator that turns any function infix. + +It is not obvious where `|` would sit in the precedence hierarchy if arithmetic was split into separate precedence levels for `+` and `-`, and `*` and `/`, whereas with haku's solution, all arithmetic expressions are simply read from left to right. + + +### Parentheses + +In case the tight-loose system is not expressive enough, parentheses can be used as an escape hatch for grouping expressions. + +```haku +2 + (2 * 2) == 2 + 2*2 +``` + + +### Functions + +Functions in haku follow the definition of mathematical functions: given a set of _arguments_, the arguments are substituted into the function's _parameter variables_, and a result is computed from the resulting expression. + +A function literal is written down like `\params -> result`, where `params` is a comma-separated list of parameters, and `result` is the function's resulting expression. + +```haku +square: \x -> x * x +``` + +A newline is permitted after the arrow `->`, which should be preferred for most functions. + +```haku +magnitude: \v -> + hypot vecX.v vecY.v + +normalize: \v -> + l = magnitude v + v / vec l l +``` + +Note that there must be at least one parameter. +In case you need a function with no parameters, you almost always want a constant value instead. + +Functions can be used by _calling_ them with space-separated arguments. +Note that space-separated function calls have higher precedence than most arithmetic operators, which means you have to use parentheses or tighten the expression to pass more complicated expressions. + +```haku +normalize (vec 4 4) +``` + +Note that a call must pass in _exactly_ the amount of arguments defined by the function. +Calling the above-defined `normalize` with more than one argument will not work: + +```haku +normalize (vec 4 4) (vec 5 5) +``` + +In places where a call with space-separated arguments is inconvenient, there are two alternative syntaxes for calling a function. + +The first is by using the `.` operator, which, when used tightly, can be used to do a function call with higher operator precedence than an ordinary space-separated call would have. + +```haku +f x == f.x +``` + +Combined with [records](#Records) and [tags](#Tags), it mimics the record field access syntax found in C-like programming languages. + +```haku +withDotter \d -> + stroke 8 #000 (line d.From d.To) + ------ ---- +``` + +The other alternative syntax is the _pipe_ `|` operator. +It calls the function on the right with the argument on the left. + +```haku +2 |sqrt +``` + +When a space-separated function call is found on the right, the `|` operator instead inserts the value from the left as the first argument of the function call. + +```haku +x |mod 2 == mod x 2 +``` + +The spacing convention around `|` is a bit unusual, but the above example demonstrates why: the `|` operator effectively turns an arbitrary function into an infix operator on par with built-in arithmetic operators. +Therefore, the function name is glued to the pipe, like `|mod`, to appear as one word visually. + +Certain functions are _built-in_, and implement core functionality that cannot be implemented in haku alone (at all, or in a performant manner). +The [haku system library](system.html) is what defines all the built-in functions. + +Due to temporary limitations of the implementation, built-in functions cannot be referenced like regular functions. +They always have to appear in a call. + +If you'd like to reference a built-in function to e.g. pass it to a list-transforming function, you will have to wrap it in a function of your own: + +```haku +add: \x, y -> x + y +sum: [1, 2, 3] |reduce 0 sum + +sin': \x -> sin x +sines: [0, pi*1/2, pi*2/2, pi*3/2] |map sin' +``` + + +### `if` expressions + +`if` expressions allow for choosing between two different results based on a condition. + +```haku +if (cond) + a +else + b +``` + +When `cond` is [_true_](#Truth), `a` will be computed and returned as the result. +Otherwise, `b` will be computed and returned as the result. + +Note that in both cases, only one of the two expressions is computed. +This allows for implementing bounded recursion to achieve repetition. + +```haku +-- Fibonacci sequence +fib: \x -> + if (x > 1) + fib n-1 + fib n-2 + else + x +``` + + +### Let expressions + +Let expressions introduce a new _variable_, or _let_, that can be referenced in the expression on the next line. + +```haku +x = y +expr +``` + +The difference between lets and [defs](#Structure-of-a-brush) is that the value of a let can change, because it can depend on non-def values, such as function arguments (therefore making it _variable_.) + +This however means that lets have reduced _scope_. +The name introduced by a def can be used in the entire program---even before the line it's introduced on---while the name introduced by a let can only be used in the expression that immediately follows the let. + +```haku +x: 1 + +f: \y -> + z = x + 1 + x + y + z -- ok + +z -- not ok +``` + +This also means that lets cannot be used to create recursive functions, because the name introduced by a let only becomes visible in its following expression. + +```haku +-- This is okay: +defib: \x -> + if (x > 1) + defib n-1 + defib n-2 + else + x + +-- This will not work: +letfib = \x -> + if (x > 1) + -- Because letfib is not yet defined at this point. + letfib n-1 + letfib n-2 + else + x + +defib 5 + letfib 5 -- It is only defined in this expression. +``` + +Since a let can be followed up by any other expression, multiple lets can be chained together. + +``` +x = 4 +y = x + 3 +x + y +``` + +Note however that `x + y` finishes the chain of lets, which means we cannot introduce additional ones after that line. +That would begin another expression! + + +## Types + +haku distinguishes values between a few different types. + +- [*nil*](#Nil) +- [*tag*](#Tags) + + - [*boolean*]{id="Boolean-type"} - either [`False` or `True`](#Tags). Indicates truth or falsehood, used in [`if` conditions](#if-expression). + +- [*number*](#Numbers) +- *vec* - a 4-dimensional vector, composed of four `number`s. +- [*rgba*](#Colors) - an RGBA color, composed of four `number`s. This is the type of color literals. +- [*function*](#Functions) +- [*list*](#Lists) +- *shape* - a mathematical shape. +- _*effect*_ - an action that can be performed by rakugaki. + + - *scribble* - something that can be drawn on the wall. + - *reticle* - an interaction the user can make with the wall. + +These types are incompatible with each other. +If you pass in a *tag* to a value that expects a *number*, you will get an error. + +You can refer to the [system library documentation](system.html) for more information on the types accepted by functions. +Note that it has a more precise notation for describing types, which explicitly documents the types of values that will be found in more nuanced situations, such as the `map` function. + + +### Truth + +Conditions in `if` expressions, as well as the `!` operator, consider certain types of values _truthy_, and others _falsy_. + +Falsy values include *nil* and the boolean `False`. +All other values are considered truthy. diff --git a/rkgk.toml b/rkgk.toml index 5cd488c..21bc332 100644 --- a/rkgk.toml +++ b/rkgk.toml @@ -23,6 +23,7 @@ import_roots = [ "docs/rkgk.dj" = "Introduction to rakugaki" "docs/system.dj" = "System library" +"docs/haku.dj" = "haku language reference" [wall_broker.default_wall_settings] From 914da923f7b5e5aee0d7a703a676a201a3e594b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=83=AA=E3=82=AD=E8=90=8C?= Date: Wed, 3 Sep 2025 16:57:10 +0200 Subject: [PATCH 13/13] update built-in examples to use newly introduced syntax --- crates/haku/src/parser.rs | 14 ++--------- crates/haku/src/token.rs | 4 ++-- docs/rkgk.dj | 49 +++++++++++++++++++++++++-------------- static/brush-box.js | 41 ++++++++++++++------------------ 4 files changed, 53 insertions(+), 55 deletions(-) diff --git a/crates/haku/src/parser.rs b/crates/haku/src/parser.rs index fa1af6d..dd2884e 100644 --- a/crates/haku/src/parser.rs +++ b/crates/haku/src/parser.rs @@ -603,29 +603,19 @@ fn is_prefix_token((kind, spaces): (TokenKind, Spaces)) -> bool { } fn prefix(p: &mut Parser) -> Closed { - let (kind, spaces) = p.peek_with_spaces(); - match kind { + match p.peek() { TokenKind::Ident => one(p, NodeKind::Ident), TokenKind::Tag => one(p, NodeKind::Tag), TokenKind::Number => one(p, NodeKind::Number), TokenKind::Color => one(p, NodeKind::Color), TokenKind::LBrack => list(p), - TokenKind::Minus if spaces.pair() == (true, false) => unary(p), + TokenKind::Minus => unary(p), TokenKind::Not => unary(p), TokenKind::LParen => paren(p), TokenKind::Backslash => lambda(p), TokenKind::If => if_expr(p), - TokenKind::Minus if spaces.pair() == (false, true) => { - let span = p.span(); - p.emit(Diagnostic::error( - span, - "`-` operator must be surrounded by an equal amount of spaces", - )); - p.advance_with_error() - } - _ => { assert!( !is_prefix_token(p.peek_with_spaces()), diff --git a/crates/haku/src/token.rs b/crates/haku/src/token.rs index 96c8282..a005630 100644 --- a/crates/haku/src/token.rs +++ b/crates/haku/src/token.rs @@ -28,8 +28,6 @@ pub enum TokenKind { Greater, GreaterEqual, Not, - Dot, - Pipe, // Punctuation Newline, @@ -42,6 +40,8 @@ pub enum TokenKind { Colon, Backslash, RArrow, + Dot, + Pipe, // Keywords Underscore, diff --git a/docs/rkgk.dj b/docs/rkgk.dj index e91e98c..6c89a89 100644 --- a/docs/rkgk.dj +++ b/docs/rkgk.dj @@ -1,5 +1,15 @@ # Introduction to rakugaki +*Warning: Neither rakugaki, nor this introductory manual is finished.* +While it will always use the most up-to-date and recommended syntax, there are things it does cover, and it will probably be rebuilt to improve coverage of features once the app stabilises a bit. + +For now, I recommend cross-referencing it with the following documents: + +- [haku language reference](haku.html) +- [System library](system.html) + +--- + Welcome to rakugaki! I hope you've been having fun fiddling with the app so far. @@ -28,7 +38,7 @@ In case you edited anything in the input box on the right, paste the following t -- and see what happens! withDotter \d -> - stroke 8 #000 (d To) + stroke 8 #000 d.To ``` rakugaki is a drawing program for digital scribbles and other pieces of art. @@ -86,8 +96,8 @@ If you want to draw multiple scribbles, you can wrap them into a list, which we -- Draw two colorful dots instead of one! withDotter \d -> [ - stroke 8 #F00 (d To + vec 4 0) - stroke 8 #00F (d To + vec -4 0) + stroke 8 #F00 (d.To + vec 4 0) + stroke 8 #00F (d.To + vec -4 0) ] ``` @@ -108,12 +118,12 @@ It'll draw the first inner list, which contains two scribbles, and then it'll dr withDotter \d -> [ [ - stroke 8 #F00 (d To + vec 4 0) - stroke 8 #00F (d To + vec -4 0) + stroke 8 #F00 (d.To + vec 4 0) + stroke 8 #00F (d.To + vec -4 0) ] [ - stroke 8 #FF0 (d To + vec 0 4) - stroke 8 #0FF (d To + vec 0 -4) + stroke 8 #FF0 (d.To + vec 0 4) + stroke 8 #0FF (d.To + vec 0 -4) ] ] ``` @@ -136,7 +146,7 @@ Recall that super simple brush from before... ```haku withDotter \d -> - stroke 8 #000 (d To) + stroke 8 #000 d.To ``` This reads as "given a dotter, output a stroke that's 8 pixels wide, has the color `#000`, and is drawn at the dotter's `To` coordinates." @@ -169,6 +179,7 @@ If you reorder or remove any one of them, your brush isn't going to work! You can also specify an alpha channel, for transparent colors---`#RRGGBBAA`, or `#RGBA`. - The third ingredient is the stroke's _position_. + `d.To` is the position of your mouse cursor. Positions in haku are represented using mathematical _vectors_, which, when broken down into pieces, are just lists of some numbers. @@ -187,22 +198,22 @@ Likewise, negative X coordinates go leftwards, and negative Y coordinates go upw --- -Vectors in haku are obtained with another function---`vec`---though we don't use it in the basic example, because `d To` already is a vector. -Vectors support all the usual math operators though, so if we wanted to, we could, for example, add a vector to `d To`, thus moving the position of the dot relative to the mouse cursor: +Vectors in haku are obtained with another function---`vec`---though we don't use it in the basic example, because `d.To` already is a vector. +Vectors support all the usual math operators though, so if we wanted to, we could, for example, add a vector to `d.To`, thus moving the position of the dot relative to the mouse cursor: ```haku withDotter \d -> - stroke 8 #000 (d To + vec 10 0) -- moved 10 pixels rightwards + stroke 8 #000 (d.To + vec 10 0) -- moved 10 pixels rightwards ``` -Also note how the `d To + vec 10 0` expression is parenthesized. +Also note how the `d.To + vec 10 0` expression is parenthesized. This is because otherwise, its individual parts would be interpreted as separate arguments to `stroke`, which is not what we want! Anyways, with all that, we let haku mix all the ingredients together, and get a black dot under the cursor. ```haku withDotter \d -> - stroke 8 #000 (d To) + stroke 8 #000 d.To ``` Nice! @@ -221,10 +232,10 @@ Let's fix that by drawing a `line` instead! ```haku withDotter \d -> - stroke 8 #000 (line (d From) (d To)) + stroke 8 #000 (line d.From d.To) ``` -We replace the singular position `d To` with a `line`. `line` expects two arguments, which are vectors defining the line's start and end points. +We replace the singular position `d.To` with a `line`. `line` expects two arguments, which are vectors defining the line's start and end points. For the starting position we use a _different_ property of `d`, which is `From`---this is the _previous_ value of `To`, which allows us to draw a continuous line. ::: aside @@ -239,8 +250,8 @@ haku also supports other kinds of shapes: circles and rectangles. ```haku withDotter \d -> [ - stroke 8 #F00 (circle (d To + vec -16 0) 16) - stroke 8 #00F (rect (d To + vec 0 -16) (vec 32 32)) + stroke 8 #F00 (circle (d.To + vec -16 0) 16) + stroke 8 #00F (rect (d.To + vec 0 -16) (vec 32 32)) ] ``` @@ -292,13 +303,15 @@ In fact, here's the full order of operations in haku for reference: 1. Tight + 1. Function applications: `.` 1. Arithmetic: `+`, `-`, `*`, `/` 1. Comparisons: `==`, `!=`, `<`, `<=`, `>`, `>=` 1. Function calls 1. Loose - 1. Arithmetic + 1. Function applications: `.` + 1. Arithmetic and pipelines `|` 1. Comparisons 1. Variables: `:`, `=` diff --git a/static/brush-box.js b/static/brush-box.js index 27750af..bee6be6 100644 --- a/static/brush-box.js +++ b/static/brush-box.js @@ -15,7 +15,7 @@ color: #000 thickness: 8 withDotter \\d -> - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) `.trim(), }, @@ -27,7 +27,7 @@ color: #000 thickness: 48 withDotter \\d -> - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) `.trim(), }, @@ -40,14 +40,10 @@ thickness: 4 length: 5 duty: 0.5 -or_: \\a, b -> - if (a) a - else b - withDotter \\d -> - visible? = mod (d Num) length < length * duty + visible? = d.Num |mod length < length * duty if (visible?) - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) else () `.trim(), @@ -61,7 +57,7 @@ color: #0003 thickness: 6 withDotter \\d -> - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) `.trim(), }, @@ -76,10 +72,10 @@ wavelength: 1 withDotter \\d -> pi = 3.14159265 - a = sin (d Num * wavelength / pi) + 1 / 2 + a = sin (d.Num * wavelength / pi) + 1 / 2 range = maxThickness - minThickness thickness = a * range + minThickness - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) `.trim(), }, @@ -93,22 +89,21 @@ amplitude: 50 wavelength: 1 mag: \\v -> - hypot (vecX v) (vecY v) + hypot vecX.v vecY.v norm: \\u -> l = mag u u / vec l l perpClockwise: \\v -> - vec (vecY v) -(vecX v) + vec vecY.v -(vecX.v) withDotter \\d -> pi = 3.14159265 - a = sin (d Num * wavelength / pi) * amplitude - direction = (d To) - (d From) - clockwise = norm (perpClockwise direction) * vec a a - from = d From + clockwise - to = d To + clockwise + a = sin (d.Num * wavelength / pi) * amplitude + clockwise = norm (perpClockwise d.To-d.From) * vec a a + from = d.From + clockwise + to = d.To + clockwise stroke thickness color (line from to) `.trim(), }, @@ -121,16 +116,16 @@ wavelength: 0.1 thickness: 8 colorCurve: \\n -> - abs (cos n) + n |cos |abs withDotter \\d -> pi = 3.14159265 l = wavelength - r = colorCurve (d Num * l) - g = colorCurve (d Num * l + pi/3) - b = colorCurve (d Num * l + 2*pi/3) + r = colorCurve (d.Num * l) + g = colorCurve (d.Num * l + pi/3) + b = colorCurve (d.Num * l + 2*pi/3) color = rgba r g b 1 - stroke thickness color (line (d From) (d To)) + stroke thickness color (line d.From d.To) `.trim(), },