Compare commits

..

13 commits

Author SHA1 Message Date
914da923f7 update built-in examples to use newly introduced syntax 2025-09-03 23:31:47 +02:00
b5cdfdb1b6 introduce haku reference docs 2025-09-03 23:31:40 +02:00
a4c18c37dc 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
2025-09-03 16:57:21 +02:00
ec7ee9626f 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)
2025-09-02 22:06:16 +02:00
69cc34d07e h1: make ' and ? only allowed as suffixes in identifiers 2025-09-02 20:52:51 +02:00
e31dde1048 add .ignore file 2025-09-02 20:52:34 +02:00
c4ad609717 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
2025-09-02 20:52:25 +02:00
958201cd18 h2: add better comment about how to use root.enableLogScope 2025-09-02 20:31:35 +02:00
2eea1f201f h2: add better VM value stack tracing 2025-09-02 20:31:15 +02:00
45099916fe update Zig code to 0.15.0 2025-09-02 20:15:41 +02:00
449f2b59df disable ligatures in code
it can be hard for first-time users to understand what these ligatures mean or how to type them
2025-09-02 20:04:09 +02:00
9808d3227f fix a few bugs with the new precedence rules 2025-09-02 20:03:41 +02:00
29a80854a4 fix parsing prefix operators in calls 2025-09-02 18:47:56 +02:00
15 changed files with 866 additions and 161 deletions

3
.ignore Normal file
View file

@ -0,0 +1,3 @@
*.ttf
*.woff2
*.png

View file

@ -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,6 +432,7 @@ unsafe extern "C" fn haku_compile_brush(
);
debug!("compiling: {closure_spec:?}");
/*
debug!("bytecode: {:?}", chunk.bytecode);
{
let mut cursor = 0_usize;
@ -446,7 +448,7 @@ unsafe extern "C" fn haku_compile_brush(
cursor += info.len as usize;
}
}
// */
instance.compile_result2 = Some(CompileResult {
defs_string: instance.defs.serialize_defs(),
tags_string: instance.defs.serialize_tags(),

View file

@ -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 {
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)?;
}
}
Ok(())
}

View file

@ -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) {
@ -208,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),

View file

@ -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<Tightness> {
let index = match kind {
TokenKind::Equal | TokenKind::Colon => 0,
// 1: reserved for `and` and `or`
@ -345,12 +327,36 @@ 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 `.`
_ if PREFIX_TOKENS.contains(kind) => 5,
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((spacing, index))
Some(match kind {
// 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.
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 is_prefix_token((kind, spaces)) => 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 +367,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 && right.0 == TokenKind::Minus && !right.1.are_balanced() {
return Tighter::Left;
}
if right_tightness > left_tightness {
Tighter::Right
} else {
@ -570,13 +582,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,
@ -584,6 +598,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 {
match p.peek() {
TokenKind::Ident => one(p, NodeKind::Ident),
@ -592,16 +610,17 @@ fn prefix(p: &mut Parser) -> Closed {
TokenKind::Color => one(p, NodeKind::Color),
TokenKind::LBrack => list(p),
TokenKind::Minus | TokenKind::Not => unary(p),
TokenKind::Minus => unary(p),
TokenKind::Not => unary(p),
TokenKind::LParen => paren(p),
TokenKind::Backslash => lambda(p),
TokenKind::If => if_expr(p),
_ => {
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();
@ -615,9 +634,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
@ -626,11 +645,14 @@ 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),
_ if PREFIX_TOKENS.contains(op.0) => infix_call(p, op),
_ if is_prefix_token(op) => infix_call(p, op),
_ => panic!("unhandled infix operator {op:?}"),
}
@ -650,7 +672,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();
}

View file

@ -40,6 +40,8 @@ pub enum TokenKind {
Colon,
Backslash,
RArrow,
Dot,
Pipe,
// Keywords
Underscore,
@ -135,6 +137,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 {

View file

@ -46,6 +46,7 @@ fn main() -> Result<(), Box<dyn Error>> {
.arg("--prominent-compile-errors")
.arg("--color")
.arg(color)
.arg("-freference-trace")
// Build output
.arg("--cache-dir")
.arg(out_path.join("zig-cache"))

View file

@ -22,6 +22,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 false
else

View file

@ -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 {
@ -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}),
},
}
}

View file

@ -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} <- {}", .{ 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} -> {}", .{ 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];
}

520
docs/haku.dj Normal file
View file

@ -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:
```
<newline>
( ) [ ] ,
= :
. |
\ ->
```
`<newline>` 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`, `a<b`, `a<=b`, `a>b`, `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.

View file

@ -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,26 +118,16 @@ 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)
]
]
```
::: 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!
@ -146,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."
@ -179,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.
@ -197,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` 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!
@ -231,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
@ -249,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)) 32 32)
stroke 8 #F00 (circle (d.To + vec -16 0) 16)
stroke 8 #00F (rect (d.To + vec 0 -16) (vec 32 32))
]
```
@ -259,6 +260,64 @@ withDotter \d ->
- `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. Function applications: `.`
1. Arithmetic: `+`, `-`, `*`, `/`
1. Comparisons: `==`, `!=`, `<`, `<=`, `>`, `>=`
1. Function calls
1. Loose
1. Function applications: `.`
1. Arithmetic and pipelines `|`
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 +329,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 +340,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 +353,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 +381,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 +414,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 +430,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 +447,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 +461,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 +623,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 +647,7 @@ splat: \d, radius ->
airbrush: \d, size ->
[
splat d size
airbrush d (size - 8)
airbrush d size-8
]
withDotter \d ->
@ -649,7 +709,7 @@ airbrush: \d, size ->
if (size > 0)
[
splat d size
airbrush d (size - 8)
airbrush d size-8
]
else
[]
@ -675,8 +735,8 @@ airbrush: \d, size ->
if (size > 0)
[
splat d size
airbrush d (size - 1)
---
airbrush d size-1
---
]
else
[]
@ -696,7 +756,7 @@ airbrush: \d, size ->
if (size > 0)
[
splat d size
airbrush d (size - 1)
airbrush d size-1
]
else
[]

View file

@ -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]

View file

@ -53,6 +53,7 @@ code,
textarea {
font-family: var(--font-monospace);
line-height: var(--line-height);
font-variant-ligatures: none;
}
/* Buttons */

View file

@ -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(),
},