diff --git a/crates/haku/src/compiler.rs b/crates/haku/src/compiler.rs index a458c5e..a095dcb 100644 --- a/crates/haku/src/compiler.rs +++ b/crates/haku/src/compiler.rs @@ -635,7 +635,7 @@ fn def_prepass<'a>(c: &mut Compiler<'a>, src: &Source<'a>, toplevel: NodeId) -> while let Some(binary) = walk.node_of(NodeKind::Binary) { let mut binary_walk = src.ast.walk(binary); if let (Some(ident), Some(op)) = (binary_walk.node(), binary_walk.get(NodeKind::Op)) { - if src.ast.span(op).slice(src.code) == ":" { + if src.ast.span(op).slice(src.code) == "=" { let name = src.ast.span(ident).slice(src.code); match c.defs.add_def(name) { Ok(_) => (), @@ -667,7 +667,7 @@ fn compile_toplevel_expr<'a>( ) -> CompileResult { if src.ast.kind(node_id) == NodeKind::Binary { if let Some(op) = src.ast.walk(node_id).get(NodeKind::Op) { - if src.ast.span(op).slice(src.code) == ":" { + if src.ast.span(op).slice(src.code) == "=" { compile_def(c, src, node_id)?; return Ok(ToplevelExpr::Def); } diff --git a/crates/haku/src/lexer.rs b/crates/haku/src/lexer.rs index 64683a9..fe8f879 100644 --- a/crates/haku/src/lexer.rs +++ b/crates/haku/src/lexer.rs @@ -73,6 +73,7 @@ fn ident(l: &mut Lexer<'_>) -> TokenKind { "or" => TokenKind::Or, "if" => TokenKind::If, "else" => TokenKind::Else, + "let" => TokenKind::Let, _ => TokenKind::Ident, } } @@ -209,7 +210,6 @@ fn token(l: &mut Lexer<'_>) -> (TokenKind, Span) { '[' => one(l, TokenKind::LBrack), ']' => one(l, TokenKind::RBrack), ',' => one(l, TokenKind::Comma), - ':' => one(l, TokenKind::Colon), '\\' => one(l, TokenKind::Backslash), _ => { diff --git a/crates/haku/src/parser.rs b/crates/haku/src/parser.rs index a64aad8..2c6a80f 100644 --- a/crates/haku/src/parser.rs +++ b/crates/haku/src/parser.rs @@ -306,7 +306,7 @@ enum Tighter { fn tighter(left: TokenKind, right: TokenKind) -> Tighter { fn tightness(kind: TokenKind) -> Option { match kind { - TokenKind::Equal | TokenKind::Colon => Some(0), + TokenKind::Equal => Some(0), TokenKind::EqualEqual | TokenKind::NotEqual | TokenKind::Less @@ -590,6 +590,7 @@ const PREFIX_TOKENS: TokenKindSet = TokenKindSet::new(&[ TokenKind::LParen, TokenKind::Backslash, TokenKind::If, + TokenKind::Let, TokenKind::LBrack, ]); @@ -605,6 +606,7 @@ fn prefix(p: &mut Parser) -> Closed { TokenKind::LParen => paren(p), TokenKind::Backslash => lambda(p), TokenKind::If => if_expr(p), + TokenKind::Let => let_expr(p), _ => { assert!( @@ -635,9 +637,7 @@ fn infix(p: &mut Parser, op: TokenKind) -> NodeKind { | TokenKind::LessEqual | TokenKind::Greater | TokenKind::GreaterEqual - | TokenKind::Colon => infix_binary(p, op), - - TokenKind::Equal => infix_let(p, op), + | TokenKind::Equal => infix_binary(p, op), _ if PREFIX_TOKENS.contains(op) => infix_call(p), @@ -666,31 +666,6 @@ fn infix_call(p: &mut Parser) -> NodeKind { NodeKind::Call } -fn infix_let(p: &mut Parser, op: TokenKind) -> NodeKind { - p.advance(); - - if p.peek() == TokenKind::Newline { - p.advance(); - } - - precedence_parse(p, op); - - if p.peek() == TokenKind::Newline { - p.advance(); - } else { - let span = p.span(); - p.emit(Diagnostic::error( - span, - "new line expected after variable definition `a = b`", - )); - p.advance_with_error(); - } - - expr(p); - - NodeKind::Let -} - pub fn expr(p: &mut Parser) { precedence_parse(p, TokenKind::Eof) } diff --git a/crates/haku/src/parser/tests.rs b/crates/haku/src/parser/tests.rs index c06d32d..c5711ed 100644 --- a/crates/haku/src/parser/tests.rs +++ b/crates/haku/src/parser/tests.rs @@ -316,7 +316,7 @@ Binary @ 0..6 ); assert_ast_eq( - "1 : 1", + "1 = 1", expr, " Binary @ 0..5 @@ -864,49 +864,52 @@ Toplevel @ 1..63 #[test] fn let_expr() { assert_ast_eq( - r#" x = 1 + r#" let x = 1 x "#, toplevel, " -Toplevel @ 1..20 - Let @ 1..20 - Ident @ 1..2 - Token @ 1..2 - Token @ 3..4 - Number @ 5..6 +Toplevel @ 1..24 + Let @ 1..24 + Token @ 1..4 + Ident @ 5..6 Token @ 5..6 - Token @ 6..7 - Ident @ 19..20 - Token @ 19..20", + Token @ 7..8 + Number @ 9..10 + Token @ 9..10 + Token @ 10..11 + Ident @ 23..24 + Token @ 23..24", ); assert_ast_eq( - r#" x = 1 - y = 2 + r#" let x = 1 + let y = 2 x + y "#, toplevel, " -Toplevel @ 1..42 - Let @ 1..42 - Ident @ 1..2 - Token @ 1..2 - Token @ 3..4 - Number @ 5..6 +Toplevel @ 1..50 + Let @ 1..50 + Token @ 1..4 + Ident @ 5..6 Token @ 5..6 - Token @ 6..7 - Let @ 19..42 - Ident @ 19..20 - Token @ 19..20 - Token @ 21..22 - Number @ 23..24 - Token @ 23..24 - Token @ 24..25 - Binary @ 37..42 - Ident @ 37..38 - Token @ 37..38 - Op @ 39..40 - Token @ 39..40 - Ident @ 41..42 - Token @ 41..42", + Token @ 7..8 + Number @ 9..10 + Token @ 9..10 + Token @ 10..11 + Let @ 23..50 + Token @ 23..26 + Ident @ 27..28 + Token @ 27..28 + Token @ 29..30 + Number @ 31..32 + Token @ 31..32 + Token @ 32..33 + Binary @ 45..50 + Ident @ 45..46 + Token @ 45..46 + Op @ 47..48 + Token @ 47..48 + Ident @ 49..50 + Token @ 49..50", ) } diff --git a/crates/haku/src/token.rs b/crates/haku/src/token.rs index 5a0ba1f..ed36150 100644 --- a/crates/haku/src/token.rs +++ b/crates/haku/src/token.rs @@ -34,7 +34,6 @@ pub enum TokenKind { RBrack, Comma, Equal, - Colon, Backslash, RArrow, @@ -44,6 +43,7 @@ pub enum TokenKind { Or, If, Else, + Let, // NOTE: This must be kept last for TokenSet to work correctly. Error, diff --git a/docs/rkgk.dj b/docs/rkgk.dj index 7335554..cfceca3 100644 --- a/docs/rkgk.dj +++ b/docs/rkgk.dj @@ -317,7 +317,7 @@ Once you define a name, its associated data stays the same throughout the entire So we can define `thickness` to be `4`, and then use it in our scribbles. ```haku -thickness: 4 +thickness = 4 withDotter \d -> [ @@ -327,11 +327,10 @@ withDotter \d -> ] ``` -`name: data` is a special operator in haku that means "whenever we say `name`, we mean `data`." -We call this operator _def_, short for _definition_. +`name = data` is a special operator in haku that tells the language "whenever we say `name`, we mean `data`." We cannot use it in arbitrary places in our program, because it wouldn't make sense. -What does it mean to have a stroke whose thickness is `meow: 5`? +What does it mean to have a stroke whose thickness is `meow = 5`? To keep a consistent program structure, haku also forces all your defs to appear _before_ your scribble. You can think of the defs as a list of ingredients for the final scribble. @@ -349,8 +348,8 @@ We'll get to why soon! Anyways, we can likewise replace our `2` constants with a def: ```haku -thickness: 4 -xOffset: 2 +thickness = 4 +xOffset = 2 withDotter \d -> [ @@ -381,9 +380,9 @@ But now there's a problem. If we change our `thickness` back to `8`, our points will overlap! ```haku -thickness: 8 - --- -xOffset: 2 +thickness = 8 + --- +xOffset = 2 withDotter \d -> [ @@ -395,9 +394,9 @@ withDotter \d -> So we'll make our `xOffset` calculated dynamically from the `thickness`, to not have to update it every time. ```haku -thickness: 8 -xOffset: thickness / 2 - ------------- +thickness = 8 +xOffset = thickness / 2 + ------------- withDotter \d -> [ @@ -485,7 +484,7 @@ That'll need fixing! Either way, let's define a function that'll make us those circles! ```haku -splat: \d, radius -> +splat = \d, radius -> fill #0001 (circle (d To) radius) withDotter \d -> @@ -535,7 +534,7 @@ haku limits the use of overloading to system functions for simplicity---adding o Since these transparent circles are so much easier to draw now, let's make a few more of them! ```haku -splat: \d, radius -> +splat = \d, radius -> fill #0001 (circle (d To) radius) withDotter \d -> @@ -581,10 +580,10 @@ Until some threshold is reached, in which case we just make a single circle. The first part is easy to do: haku allows us to define a function that calls itself without making any fuss. ```haku -splat: \d, radius -> +splat = \d, radius -> fill #0001 (circle (d To) radius) -airbrush: \d, size -> +airbrush = \d, size -> [ splat d size airbrush d (size - 8) @@ -617,9 +616,9 @@ We call this act of switching execution paths _branching_. Try this out---change the `radius`, and observe how your brush changes color once you set it beyond 16: ```haku -radius: 8 +radius = 8 -color: +color = if (radius < 16) #00F else @@ -642,10 +641,10 @@ An `if` only calculates the argument it needs to produce the result. This allows us to use it to prevent unbounded recursion in our `airbrush` example. ```haku -splat: \d, radius -> +splat = \d, radius -> fill #0001 (circle (d To) radius) -airbrush: \d, size -> +airbrush = \d, size -> if (size > 0) [ splat d size @@ -668,10 +667,10 @@ But the airbrush still looks super primitive. Let's try increasing the fidelity by doing smaller steps! ```haku -splat: \d, radius -> +splat = \d, radius -> fill #0001 (circle (d To) radius) -airbrush: \d, size -> +airbrush = \d, size -> if (size > 0) [ splat d size @@ -688,11 +687,11 @@ withDotter \d -> Well... sure, that's just a black blob with a slight gradient on the outer edge, so let's decrease the opacity. ```haku -splat: \d, radius -> +splat = \d, radius -> fill #00000004 (circle (d To) radius) --------- -airbrush: \d, size -> +airbrush = \d, size -> if (size > 0) [ splat d size @@ -741,7 +740,7 @@ Most commonly, colors are blended using _linear interpolation_---which is essent Mathematically, linear interpolation is defined using this formula: ```haku -lerp: \a, b, t -> +lerp = \a, b, t -> a + (b - a) * t ``` @@ -807,30 +806,12 @@ For now you'll have to construct your brushes with this in mind. ### And more limits -There are more limits on top of this, which stem from rakugaki's design. +There are more limits on top of this, which stem from haku's design. +Since it's running _your_ code on _my_ server, it has some arbitrary limits set to prevent it from causing much harm. -Recall that rakugaki is multiplayer! -You can draw with your friends. -But, your friends may not have a computer as good as yours. -So to keep the experience fair, haku sets some limits on brush code. - -A brush cannot be too long, and it cannot execute too long. -It also cannot consume too much memory---you cannot have too many definitions, or too many temporary values at once. - -If you ever brush (ha ha) up against these limits, you'll see colorful bars appear beside the brush preview in the bottom right corner. -These bars show you how much you're nearing the limits! - -Try adding this line to your brush: - -```haku -r: range 1 30000 -``` - -`range 1 30000` generates a list of integers between 1 and 30000, without having you write them out one by one. -But we can also see that it consumes a bunch of _fuel_ (units of execution time), and _a lot_ of memory---almost all, in fact! - -Code size is harder to run up against, because it requires writing a pretty huge amount of characters into the editor. -Feel free to try it out yourself---try writing out some really long lists by hand, and see what happens! +haku code cannot be too long, and it cannot execute too long. +It cannot consume too much memory---you cannot have too many definitions, or too many temporary values at once. +There are also memory usage limits on "heavyweight" data, such as functions or lists. ## Reticles @@ -858,17 +839,17 @@ This allows you to _animate_ your brushes over time! For example, this brush draws a rainbow. ```haku -colorCurve: \n -> +colorCurve = \n -> abs (cos n) -pi: 3.14159265 -l: 0.1 -- wavelength +pi = 3.14159265 +l = 0.1 -- wavelength withDotter \d -> - 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 + let r = colorCurve (d Num * l) + let g = colorCurve (d Num * l + pi/3) + let b = colorCurve (d Num * l + 2*pi/3) + let color = rgba r g b 1 stroke 8 color (line (d From) (d To)) ``` @@ -878,28 +859,28 @@ Currently, `withDotter` is the only reticle available in rakugaki, and it cannot In the future rakugaki might get reticles that let you select lines, rectangles, ellipses, curves... but today is not that day. -### What's that, `=`? +### What's that, `let`? I mentioned before that you cannot have defs inside functions. -What you _can_ have though, is _variables_, defined with `name = data`. +What you _can_ have though, is `let`s, which define _variables_. Unlike defs, which are constant and cannot vary, variables' values can depend on function parameters---and a function can be called with a different set of parameters each time, thus making them variable! -A variable always takes the following form. +A `let` always takes the following form. ```haku -name = value +let name = value then ``` It's very similar to a def, with one major difference. -Because a variable by itself only _names a value_ and does not have a result, it must be followed by another expression on the following line---and that expression determines the result. -The magic is that this continuing expression can refer to the `name` we had previously assigned in the `name = value` expression. +Because a `let` by itself only _names a value_ and does not have a result, it must be followed by another expression on the following line---and that expression determines the result. +The magic is that this continuing expression can refer to the `name` we had previously assigned in the `let` expression. ::: aside -Here's a bit of trivia: variables are exactly the same as function parameters! -The `name = value` expression above is equivalent to applying the argument `value` to a function taking in the parameter `name`, and returning `then` as the result. +Here's a bit of trivia: the variable defined by a `let` is exactly the same as a function parameter. +The `let` above is equivalent to applying the argument `value` to a function taking in the parameter `name`, and returning `then` as the result. ```haku (\name -> then) value @@ -912,15 +893,15 @@ That's right. haku is a cute little Haskell for artists. ::: -Variables aren't only useful for reusability---they're also helpful for breaking your brushes into smaller, more digestible pieces! +`let`s aren't only useful for reusability---they're also helpful for breaking your brushes into smaller, more digestible pieces! Compare the above version of the rainbow brush to this version, where all the `let`s are written inline: ```haku -colorCurve: \n -> +colorCurve = \n -> abs (cos n) -pi: 3.14159265 -l: 0.1 -- wavelength +pi = 3.14159265 +l = 0.1 -- wavelength withDotter \d -> stroke 8 (rgba (colorCurve (d Num * l)) (colorCurve (d Num * l + pi/3)) (colorCurve (d Num * l + 2*pi/3)) 1) (line (d From) (d To)) @@ -931,41 +912,6 @@ That's one hard to read beast of a `stroke`! Generally, if a line is so long it wraps around rakugaki's narrow little text editor, it's probably a good idea to split it into variables. -### So... sometimes it's `:`, sometimes it's `=`, help, I'm confused! - -If you're having trouble understanding when to use `:` and when to use `=`, here's a short version: - -- `:` defines a name across your whole program. -- `=` defines a name that's only visible on the next line (more or less.) - -Since `:` defines a name accessible from your whole program, it cannot access function parameters---which are temporary and local. -The opposite of "accessible in the whole program"! - -As for `=`, I said it defines a name that's only visible on the next line "_more or less_", because technically the line that follows can actually be broken up into multiple lines, as would be the case with e.g. an `if`: - -```haku -color: #000 -thickness: 4 -length: 5 -duty: 0.5 - -or_: \a, b -> -- haku doesn't have a boolean OR yet... - if (a) a - else b - -withDotter \d -> - visible = mod (d Num) length < length * duty - if (visible) - -- this is more than one line, and `visible` can still be used here! - stroke thickness color (line (d From) (d To)) - else - () -- ...and here, too! -``` - -Another thing: as shown in the examples above, you can chain multiple `=` expressions together, which also breaks this "next line" rule of thumb. -But it should still help in building an intuition and spotting the patterns! - - ## Have fun With that said, I hope you can have fun with rakugaki despite it being in its infancy! diff --git a/docs/system.dj b/docs/system.dj index 1b9f474..03a2a4b 100644 --- a/docs/system.dj +++ b/docs/system.dj @@ -25,6 +25,8 @@ Operators may have one or two arguments, where one argument corresponds to a pre Note that this documentation lists a unary and binary operator of the same spelling as _two separate functions_, not overloads of a single function. The argument name usually does not matter when calling the function - it is only used for documentation purposes. +The one exception is arguments called `...`, which signify that zero or more arguments can be passed to the function at that position. +(Currently there are no functions that accept any number of arguments, though.) The argument _type_ however is important. If you try to use a function with the wrong type of value as its argument, it will fail with an error. @@ -565,7 +567,7 @@ For example, consider multiplicatively blending two colors. ```haku -- This is how you can multiply two colors together. -- Note that the `*` operator works for colors, so you don't need to define this in your brushes. -mulRgba: \a, b -> +mulRgba = \a, b -> rgba (rgbaR a * rgbaR b) (rgbaG a * rgbaG b) (rgbaB a * rgbaB b) (rgbaA a * rgbaA b) ``` @@ -573,11 +575,11 @@ If haku represented colors using an 8-bit `0` to `255` range instead, to multipl ```haku -- NOTE: This example does NOT work correctly. -mulRgba: \a, b -> - red = (rgbaR a * rgbaR b) / 255 - green = (rgbaG a * rgbaG b) / 255 - blue = (rgbaB a * rgbaB b) / 255 - alpha = (rgbaA a * rgbaA b) / 255 +mulRgba = \a, b -> + let red = (rgbaR a * rgbaR b) / 255 + let green = (rgbaG a * rgbaG b) / 255 + let blue = (rgbaB a * rgbaB b) / 255 + let alpha = (rgbaA a * rgbaA b) / 255 rgba red green blue alpha ``` @@ -681,38 +683,32 @@ Some of these operations may be a bit confusing, so here are some examples. ```haku -- To add two to all elements in a list: -list: range 1 4 -- [1, 2, 3, 4] -twoAdded: map list \x -> +list = range 1 4 -- [1, 2, 3, 4] +twoAdded = map list \x -> x + 2 --- [3, 4, 5, 6] ``` ```haku -- To filter out only even numbers in a list: -list: range 1 10 -- [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] -isEven: \x -> mod x 2 == 0 -onlyEven: filter list isEven --- [2, 4, 6, 8, 10] +list = range 1 10 +isEven = \x -> mod x 2 == 0 +onlyEven = filter list isEven ``` ```haku -- To sum all the numbers in a list: -list: [1, 3, 10, 2, 30, 4, 1] -sum: reduce list 0 \acc, value -> acc + value --- 51 +list = [1, 3, 10, 2, 30, 4, 1] +sum = reduce list 0 \acc, value -> acc + value ``` ```haku -- To flatten a singly-nested list: -list: [[1, 2], [3, 4], [5, 6]] -flatList: flatten list -- [1, 2, 3, 4, 5, 6] +list = [[1, 2], [3, 4], [5, 6]] +flatList = flatten list -- [1, 2, 3, 4, 5, 6] -- Note that this only applies to a single level of nesting: -deepList: [[[1, 2, 3, 4]]] -lessDeepList: flatten deepList -- [[1, 2, 3, 4]] - --- This can be used to join lists together without nesting: -join: \a, b -> flatten [a, b] +deepList = [[[1, 2, 3, 4]]] +lessDeepList = flatten deepList -- [[1, 2, 3, 4]] ``` diff --git a/static/brush-box.js b/static/brush-box.js index 92d01d7..e995e0a 100644 --- a/static/brush-box.js +++ b/static/brush-box.js @@ -11,8 +11,8 @@ export const builtInPresets = [ code: ` -- Try playing around with the values -- and see what happens! -color: #000 -thickness: 8 +color = #000 +thickness = 8 withDotter \\d -> stroke thickness color (line (d From) (d To)) @@ -23,42 +23,20 @@ withDotter \\d -> id: "builtin/thick", name: "Thick", code: ` -color: #000 -thickness: 48 +color = #000 +thickness = 48 withDotter \\d -> stroke thickness color (line (d From) (d To)) `.trim(), }, - { - id: "builtin/dashes", - name: "Dashes", - code: ` -color: #000 -thickness: 4 -length: 5 -duty: 0.5 - -or_: \\a, b -> - if (a) a - else b - -withDotter \\d -> - visible = mod (d Num) length < length * duty - if (visible) - stroke thickness color (line (d From) (d To)) - else - () - `.trim(), - }, - { id: "builtin/pencil", name: "Pencil", code: ` -color: #0003 -thickness: 6 +color = #0003 +thickness = 6 withDotter \\d -> stroke thickness color (line (d From) (d To)) @@ -69,16 +47,16 @@ withDotter \\d -> id: "builtin/woobly", name: "Woobly", code: ` -color: #000 -minThickness: 8 -maxThickness: 20 -wavelength: 1 +color = #000 +minThickness = 8 +maxThickness = 20 +wavelength = 1 withDotter \\d -> - pi = 3.14159265 - a = (sin (d Num * wavelength / pi) + 1) / 2 - range = maxThickness - minThickness - thickness = minThickness + a * range + let pi = 3.14159265 + let a = (sin (d Num * wavelength / pi) + 1) / 2 + let range = maxThickness - minThickness + let thickness = minThickness + a * range stroke thickness color (line (d From) (d To)) `.trim(), }, @@ -87,28 +65,28 @@ withDotter \\d -> id: "builtin/wavy", name: "Wavy", code: ` -color: #000 -thickness: 4 -amplitude: 50 -wavelength: 1 +color = #000 +thickness = 4 +amplitude = 50 +wavelength = 1 -mag: \\v -> +mag = \\v -> hypot (vecX v) (vecY v) -norm: \\u -> - l = mag u +norm = \\u -> + let l = mag u u / vec l l -perpClockwise: \\v -> +perpClockwise = \\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 + let pi = 3.14159265 + let a = sin (d Num * wavelength / pi) * amplitude + let direction = (d To) - (d From) + let clockwise = norm (perpClockwise direction) * vec a a + let from = d From + clockwise + let to = d To + clockwise stroke thickness color (line from to) `.trim(), }, @@ -117,19 +95,19 @@ withDotter \\d -> id: "builtin/rainbow", name: "Rainbow", code: ` -wavelength: 0.1 -thickness: 8 +wavelength = 0.1 +thickness = 8 -colorCurve: \\n -> +colorCurve = \\n -> abs (cos n) 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) - color = rgba r g b 1 + let pi = 3.14159265 + let l = wavelength + let r = colorCurve (d Num * l) + let g = colorCurve (d Num * l + pi/3) + let b = colorCurve (d Num * l + 2*pi/3) + let color = rgba r g b 1 stroke thickness color (line (d From) (d To)) `.trim(), },