Compare commits
13 commits
b52c1b26c9
...
914da923f7
| Author | SHA1 | Date | |
|---|---|---|---|
| 914da923f7 | |||
| b5cdfdb1b6 | |||
| a4c18c37dc | |||
| ec7ee9626f | |||
| 69cc34d07e | |||
| e31dde1048 | |||
| c4ad609717 | |||
| 958201cd18 | |||
| 2eea1f201f | |||
| 45099916fe | |||
| 449f2b59df | |||
| 9808d3227f | |||
| 29a80854a4 |
15 changed files with 866 additions and 161 deletions
3
.ignore
Normal file
3
.ignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
*.ttf
|
||||
*.woff2
|
||||
*.png
|
||||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -361,14 +361,18 @@ 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 == "=" {
|
||||
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",
|
||||
"defs `a: b` may only appear at the top level",
|
||||
));
|
||||
return Ok(());
|
||||
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) {
|
||||
|
|
@ -379,34 +383,21 @@ fn compile_binary<'a>(c: &mut Compiler<'a>, src: &Source<'a>, node_id: NodeId) -
|
|||
} else {
|
||||
c.emit(Diagnostic::error(
|
||||
src.ast.span(op),
|
||||
"this unary operator is currently unimplemented",
|
||||
"this binary 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(())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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"))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
520
docs/haku.dj
Normal 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.
|
||||
138
docs/rkgk.dj
138
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,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,7 +353,7 @@ 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,7 +735,7 @@ 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
|
||||
[]
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ code,
|
|||
textarea {
|
||||
font-family: var(--font-monospace);
|
||||
line-height: var(--line-height);
|
||||
font-variant-ligatures: none;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
},
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue