├── passerine-aspen ├── src │ ├── add.rs │ ├── bench.rs │ ├── debug.rs │ ├── doc.rs │ ├── test.rs │ ├── publish.rs │ ├── update.rs │ ├── run.rs │ ├── cli.rs │ ├── main.rs │ ├── status.rs │ ├── repl.rs │ ├── new.rs │ └── manifest.rs ├── .gitignore ├── Cargo.toml └── README.md ├── passerine ├── src │ ├── compiler │ │ ├── unify.rs │ │ ├── expand.rs │ │ ├── desugar.rs │ │ ├── mod.rs │ │ └── syntax.rs │ ├── construct │ │ ├── mod.rs │ │ ├── symbol.rs │ │ ├── scope.rs │ │ ├── token.rs │ │ └── tree.rs │ ├── vm │ │ ├── mod.rs │ │ ├── trace.rs │ │ ├── slot.rs │ │ ├── stack.rs │ │ └── tag.rs │ ├── kernel │ │ ├── mod.rs │ │ └── logic.rs │ └── lib.rs ├── tests │ ├── snippets │ │ ├── group.pn │ │ ├── before_assign.pn │ │ ├── double_capture.pn │ │ ├── recursive.pn │ │ ├── identity.pn │ │ ├── tuple.pn │ │ ├── tuple_lambda.pn │ │ ├── chaining.pn │ │ ├── tuples.pn │ │ ├── closure.pn │ │ ├── call.pn │ │ ├── tuple_compose.pn │ │ ├── tuple_order.pn │ │ └── texpr.pn │ ├── regressions │ │ ├── a_match.pn │ │ ├── equal.pn │ │ ├── integer.pn │ │ ├── add_numbers.pn │ │ ├── neg.pn │ │ ├── hello.pn │ │ ├── a_macro.pn │ │ ├── remainder.pn │ │ ├── math.pn │ │ ├── constant_macro.pn │ │ ├── syntax_call.pn │ │ ├── use_before.pn │ │ ├── ambiguious.pn │ │ ├── pattern_label.pn │ │ ├── type_decl.pn │ │ ├── bare_macro.pn │ │ ├── bad_call.pn │ │ ├── magic.pn │ │ ├── to_string.pn │ │ ├── swap.pn │ │ ├── hoist.pn │ │ ├── multiple_macros.pn │ │ ├── macro_in_macro.pn │ │ ├── or.pn │ │ ├── forever.pn │ │ ├── milk.pn │ │ ├── odd_even.pn │ │ ├── match.pn │ │ └── fibonacci.pn │ └── fledgling.rs ├── std │ ├── kernel.pn │ └── mod.pn ├── .gitignore ├── proptest-regressions │ └── compiler │ │ ├── lex.txt │ │ └── read.txt ├── Cargo.toml └── CONTRIBUTING.md ├── passerine-qualm ├── .gitignore ├── src │ ├── stack.rs │ ├── fiber.rs │ ├── main.rs │ ├── slot.rs │ ├── heap │ │ ├── pointer.rs │ │ ├── range_set.rs │ │ └── mod.rs │ └── code.rs └── Cargo.toml ├── Cargo.toml ├── .gitignore ├── passerine-common ├── Cargo.toml └── src │ ├── lib.rs │ ├── effect.rs │ ├── ty.rs │ ├── code.rs │ ├── closure.rs │ ├── source.rs │ ├── lit.rs │ ├── inject.rs │ ├── number.rs │ ├── module.rs │ ├── opcode.rs │ ├── data.rs │ ├── lambda.rs │ └── span.rs ├── passerine-derive ├── Cargo.toml └── src │ └── lib.rs ├── Logo.svg ├── LICENSE ├── README.md └── Logotype.svg /passerine-aspen/src/add.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /passerine-aspen/src/bench.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /passerine-aspen/src/debug.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /passerine-aspen/src/doc.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /passerine-aspen/src/test.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /passerine/src/compiler/unify.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /passerine-aspen/src/publish.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /passerine-aspen/src/update.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /passerine-qualm/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /passerine/tests/snippets/group.pn: -------------------------------------------------------------------------------- 1 | # action: parse 2 | # outcome: success 3 | 4 | a {} 5 | -------------------------------------------------------------------------------- /passerine/tests/regressions/a_match.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | 4 | Look 7 5 | -------------------------------------------------------------------------------- /passerine/tests/snippets/before_assign.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: syntax 3 | 4 | x 5 | x = 0 6 | -------------------------------------------------------------------------------- /passerine/src/construct/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod scope; 2 | pub mod symbol; 3 | pub mod token; 4 | pub mod tree; 5 | -------------------------------------------------------------------------------- /passerine/tests/regressions/equal.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: false 4 | 5 | "Banana" == "Nice" 6 | -------------------------------------------------------------------------------- /passerine/tests/regressions/integer.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 2 4 | 5 | println (1 + 3 - 2) 6 | -------------------------------------------------------------------------------- /passerine/tests/regressions/add_numbers.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 3.0 4 | 5 | println (1.0 + 2.0) 6 | -------------------------------------------------------------------------------- /passerine/tests/snippets/double_capture.pn: -------------------------------------------------------------------------------- 1 | # action: gen 2 | # outcome: success 3 | 4 | a = "Heck" 5 | 6 | () -> a 7 | () -> a 8 | -------------------------------------------------------------------------------- /passerine/tests/regressions/neg.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 4 4 | 5 | x = -4 6 | println x 7 | println (-x) 8 | -------------------------------------------------------------------------------- /passerine/tests/snippets/recursive.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | 4 | foo = 0 5 | x = () -> bar foo 6 | bar = 0 7 | x () 8 | -------------------------------------------------------------------------------- /passerine/tests/regressions/hello.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: "Hello, World!" 4 | 5 | println "Hello, World!" 6 | -------------------------------------------------------------------------------- /passerine/tests/snippets/identity.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 7.0 4 | 5 | identity = x -> x 6 | identity 7.0 7 | -------------------------------------------------------------------------------- /passerine-qualm/src/stack.rs: -------------------------------------------------------------------------------- 1 | pub struct Frame { 2 | 3 | } 4 | 5 | pub struct Stack { 6 | data: Vec, 7 | frames: Vec, 8 | } 9 | -------------------------------------------------------------------------------- /passerine/tests/regressions/a_macro.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 7 4 | 5 | syntax 'boop { 7 } 6 | syntax 'bop { boop } 7 | bop 8 | -------------------------------------------------------------------------------- /passerine/tests/snippets/tuple.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: "Post" 4 | 5 | (a, b,) = "Nice", "Post" 6 | b, a = a, b 7 | 8 | a 9 | -------------------------------------------------------------------------------- /passerine/tests/snippets/tuple_lambda.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 4.0 4 | 5 | a, b, c = True, x -> x, False 6 | 7 | (b 4.0) 8 | -------------------------------------------------------------------------------- /passerine/tests/regressions/remainder.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 1.0 4 | 5 | # TODO: negative behaviour 6 | println (3.0 % 2.0) 7 | -------------------------------------------------------------------------------- /passerine-qualm/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "flex" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | attorand = "1.0" 8 | rayon = "1.5" 9 | -------------------------------------------------------------------------------- /passerine/tests/regressions/math.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 15.5 4 | 5 | 5.0 * 4.0 + 3.0 / 2.0 + 1.0 - 2.0 + 1.0 * (3.0 - 2.0 * 4.0) 6 | -------------------------------------------------------------------------------- /passerine/tests/regressions/constant_macro.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: false 4 | 5 | syntax 'not_true { x -> false } 6 | 7 | true . not_true 8 | -------------------------------------------------------------------------------- /passerine/tests/regressions/syntax_call.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: trace 3 | 4 | syntax 'calls w { 5 | () -> (w -> w + ()) 6 | } 7 | 8 | (calls x) () 10.0 9 | -------------------------------------------------------------------------------- /passerine/tests/snippets/chaining.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: "Bye" 4 | 5 | i = x -> x 6 | bye = x -> "Bye" 7 | 8 | "Hello" |> i |> bye |> i 9 | -------------------------------------------------------------------------------- /passerine/tests/snippets/tuples.pn: -------------------------------------------------------------------------------- 1 | # action: gen 2 | # outcome: success 3 | 4 | () # empty 5 | (True,) # unary 6 | (True, False) # binary 7 | (1.0, True, "Hello") # trinary 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | 3 | members = [ 4 | "passerine-common", 5 | "passerine-derive", 6 | "passerine", 7 | "passerine-aspen", 8 | # "passerine-qualm", 9 | ] -------------------------------------------------------------------------------- /passerine/tests/regressions/use_before.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: trace 3 | 4 | # TODO: should be syntax?? 5 | 6 | # function = () -> value 7 | # value 8 | # value = 7.0 9 | -------------------------------------------------------------------------------- /passerine/tests/regressions/ambiguious.pn: -------------------------------------------------------------------------------- 1 | # action: desugar 2 | # outcome: syntax 3 | 4 | syntax a 'where b { 0.0 } 5 | 6 | a = 1.0 7 | b = 2.0 8 | 9 | a where b where a 10 | -------------------------------------------------------------------------------- /passerine/tests/regressions/pattern_label.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: "yellow" 4 | 5 | my_banana = Banana "yellow" 6 | Banana color = my_banana 7 | color 8 | -------------------------------------------------------------------------------- /passerine/tests/regressions/type_decl.pn: -------------------------------------------------------------------------------- 1 | print Cool 7 2 | 3 | x = () -> { 4 | print Cool 7 5 | } 6 | x () 7 | 8 | print Cool 7 9 | print Cool 7 10 | 11 | 0/0 12 | 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore: 2 | **/.* 3 | **/target 4 | **/Cargo.lock 5 | 6 | # Protip: create a scratchpad to mess around with stuff! 7 | **/scratchpad 8 | 9 | # But not: 10 | !.gitignore 11 | !.git 12 | !.github -------------------------------------------------------------------------------- /passerine-aspen/.gitignore: -------------------------------------------------------------------------------- 1 | # Build folders 2 | /target 3 | /example 4 | 5 | # Backups, rustfmt 6 | **/*.rs.bk 7 | 8 | # Hidden files 9 | **/.* 10 | 11 | # But not: 12 | !.gitignore 13 | !.git 14 | !.github 15 | -------------------------------------------------------------------------------- /passerine/std/kernel.pn: -------------------------------------------------------------------------------- 1 | effect Write 2 | 3 | effect Show 4 | 5 | effect Choice 6 | 7 | a b c = d e f 8 | ((a b c) = (d e f)) 9 | 10 | println = item -> { 11 | string = Show item + "\n" 12 | Write string 13 | } -------------------------------------------------------------------------------- /passerine/tests/snippets/closure.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 3.14 4 | 5 | pi = 2.7 6 | 7 | update_a = () -> () -> { pi = 3.14 } 8 | 9 | updater = update_a () 10 | updater () 11 | 12 | pi 13 | -------------------------------------------------------------------------------- /passerine/tests/snippets/call.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: "Nice" 4 | 5 | a = x -> x "Nice" 6 | b = x -> a x 7 | c = x -> b x 8 | d = x -> c x 9 | e = x -> d x 10 | 11 | i = x -> x 12 | 13 | e i 14 | -------------------------------------------------------------------------------- /passerine/.gitignore: -------------------------------------------------------------------------------- 1 | # Build folders 2 | /target 3 | 4 | # Backups, rustfmt 5 | **/*.rs.bk 6 | 7 | # Hidden files 8 | **/.* 9 | 10 | # scratchpad 11 | /scratchpad 12 | 13 | # But not: 14 | !.gitignore 15 | !.git 16 | !.github 17 | -------------------------------------------------------------------------------- /passerine/tests/regressions/bare_macro.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 14.0 4 | 5 | add3 = x y z -> x + y + z 6 | 7 | syntax 'pi { 3.5 } 8 | 9 | # TODO: should `add3 pi 7 pi` be valid? 10 | add3 (pi) 7.0 (pi) 11 | -------------------------------------------------------------------------------- /passerine/tests/regressions/bad_call.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: trace 3 | 4 | uh = problemo -> (true -> false) false == true 5 | hi = w -> uh "Hi " + w 6 | im = w -> hi "I'm " + w 7 | joe = w -> im "Joe" + w 8 | 9 | println joe "." 10 | -------------------------------------------------------------------------------- /passerine/tests/regressions/magic.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | 4 | println (magic "add" (1.0, 5.0)) 5 | 6 | magic "println" { 7 | "Hello, " + "World!" 8 | } 9 | 10 | double = x -> x + x 11 | 12 | 5.0 . double . println 13 | -------------------------------------------------------------------------------- /passerine/tests/regressions/to_string.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: "I am 5.5 years old" 4 | 5 | to_string = data -> magic "to_string" data 6 | 7 | age = 5.5 8 | 9 | println ("I am " + age.to_string + " years old") 10 | -------------------------------------------------------------------------------- /passerine/tests/regressions/swap.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: false 4 | 5 | syntax a 'swap b { 6 | tmp = a 7 | a = b 8 | b = tmp 9 | } 10 | 11 | tmp = () 12 | x = true 13 | y = false 14 | 15 | x swap y 16 | 17 | x 18 | -------------------------------------------------------------------------------- /passerine/tests/regressions/hoist.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 5.86 4 | 5 | flubber = () -> { 6 | foo = () -> { 7 | println (pi + e) 8 | } 9 | pi = 3.14 10 | foo () 11 | } 12 | e = 2.72 13 | 14 | flubber () 15 | -------------------------------------------------------------------------------- /passerine/tests/snippets/tuple_compose.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: true 4 | 5 | b = False -> True 6 | 7 | # is the same as (x, y) = (a, (b.c)) 8 | # not (x, y) = ((a, b) . c) 9 | x, y = "Hello", False |> b 10 | 11 | # println y 12 | -------------------------------------------------------------------------------- /passerine/tests/regressions/multiple_macros.pn: -------------------------------------------------------------------------------- 1 | # action: desugar 2 | # outcome: syntax 3 | 4 | syntax 'macro1 x { 5 | println "In macro1!" 6 | } 7 | 8 | syntax x 'macro2 { 9 | println "In macro2!" 10 | } 11 | 12 | macro1 macro2 # which one do you call? 13 | -------------------------------------------------------------------------------- /passerine/tests/snippets/tuple_order.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 10.0 4 | 5 | last = 0.0 6 | 7 | tuple = ( 8 | { last = 1.0; 3.0 }, 9 | { last = 2.0; 5.0 }, # last should be 2.0 10 | ) 11 | 12 | three, five = tuple 13 | 14 | (last, three, five) 15 | -------------------------------------------------------------------------------- /passerine/tests/regressions/macro_in_macro.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 2.0 4 | 5 | syntax a 'swap b { 6 | a, b = b, a 7 | } 8 | 9 | syntax a 'rot b 'rot c { 10 | a swap b 11 | b swap c 12 | } 13 | 14 | a = 1.0 15 | b = 2.0 16 | c = 3.0 17 | 18 | a rot b rot c 19 | 20 | println a 21 | -------------------------------------------------------------------------------- /passerine/src/vm/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the core VM implementation. 2 | //! Note that these modules are public for documentation visiblility, 3 | //! But should never be used outside of the module by `common` or `compiler`. 4 | 5 | pub mod fiber; 6 | 7 | pub mod slot; 8 | pub mod stack; 9 | pub mod tag; 10 | pub mod trace; 11 | -------------------------------------------------------------------------------- /passerine-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passerine-common" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | # I would put this under dev-dependencies if I could. 10 | proptest-derive = "0.3.0" 11 | proptest = "1.0.0" -------------------------------------------------------------------------------- /passerine-qualm/src/fiber.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeMap; 2 | use crate::{Stack, Heap}; 3 | 4 | pub struct HandlerId(usize); 5 | 6 | pub struct Handler { 7 | fiber: Fiber, 8 | } 9 | 10 | pub struct Fiber { 11 | handlers: BTreeMap, 12 | stack: Stack, 13 | heap: Heap, 14 | parent: FiberId, 15 | } 16 | -------------------------------------------------------------------------------- /passerine/tests/regressions/or.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: true 4 | 5 | syntax a 'or b { 6 | magic "if" (a, 7 | true, 8 | b, 9 | ) 10 | } 11 | 12 | syntax a 'equals b { 13 | magic "equal" (a, b) 14 | } 15 | 16 | x = 1.0 17 | y = 2.0 18 | 19 | result = (x equals 5.0) or (y equals 2.0) 20 | 21 | println result 22 | -------------------------------------------------------------------------------- /passerine-derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passerine-derive" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | 8 | [dependencies] 9 | passerine-common = { path = "../passerine-common" } 10 | proc-macro2 = "1.0" 11 | syn = "1.0" 12 | quote = "1.0" 13 | 14 | [lib] 15 | proc-macro = true 16 | -------------------------------------------------------------------------------- /passerine/tests/regressions/forever.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | 4 | zero = f x -> x 5 | succ = n f x -> f (n f x) 6 | 7 | println_number = number -> { 8 | number { x -> println " o " } () 9 | println "---" 10 | } 11 | 12 | syntax 'loop do { 13 | l = () -> { do; l () } 14 | l () 15 | } 16 | 17 | counter = zero 18 | 19 | # loop { 20 | # println_number counter 21 | # counter = succ counter 22 | # } 23 | -------------------------------------------------------------------------------- /Logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /passerine/src/kernel/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module provides the standard/core language library 2 | //! And compiler-magic FFI bindings. 3 | 4 | // pub mod io; 5 | // pub mod control; 6 | // pub mod logic; 7 | 8 | use passerine_derive::Effect; 9 | 10 | use crate::common::data::Data; 11 | 12 | #[derive(Effect)] 13 | pub struct Write(Data); 14 | 15 | // #[derive(Effect)] 16 | // pub struct Writeln(Data); 17 | 18 | #[derive(Effect)] 19 | pub struct Show(Data); 20 | 21 | #[derive(Effect)] 22 | pub struct Choice { 23 | cond: bool, 24 | then: Data, 25 | other: Data, 26 | } 27 | -------------------------------------------------------------------------------- /passerine/proptest-regressions/compiler/lex.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc e7ac9b2ee9f3c1fb428c53081a42452d8cefb2b6db3dcd37ff4ebeb1663a0b8b # shrinks to s = "" 8 | cc 5f545768b20d72d8d8143ae39973c7e69f4caaab49d42153d23782ad83b04ee9 # shrinks to s = "\"\\᪠\u{16f4f}" 9 | cc 7d78aaabe932cf464a675ab5998388babc2548031795409ac15f45d609c13387 # shrinks to s = "#" 10 | -------------------------------------------------------------------------------- /passerine/src/kernel/logic.rs: -------------------------------------------------------------------------------- 1 | use passerine_derive::Effect; 2 | 3 | use crate::common::Data; 4 | 5 | // TODO: implement equality rather than just deriving PartialEq on Data. 6 | 7 | // Rust hit it right on the nose with the difference between equality and 8 | // partial equality TODO: equality vs partial equality in passerine? 9 | 10 | #[derive(Effect)] 11 | pub struct Equal(Data, Data); 12 | 13 | #[derive(Effect)] 14 | pub struct Less(Data, Data); 15 | 16 | #[derive(Effect)] 17 | pub struct Greater(Data, Data); 18 | 19 | #[derive(Effect)] 20 | pub struct LessEqual(Data, Data); 21 | 22 | #[derive(Effect)] 23 | pub struct GreaterEqual(Data, Data); 24 | -------------------------------------------------------------------------------- /passerine-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Contains datastructures and utility functions 2 | //! common to both the `compiler` and `vm`. 3 | //! 4 | //! - Core data-strucutres. 5 | //! - Opcodes and number splicing. 6 | //! - Source code representation and span annotations. 7 | 8 | pub mod closure; 9 | pub mod data; 10 | pub mod effect; 11 | pub mod inject; 12 | pub mod lambda; 13 | pub mod lit; 14 | pub mod module; 15 | pub mod number; 16 | pub mod opcode; 17 | pub mod source; 18 | pub mod span; 19 | pub mod ty; 20 | 21 | pub use closure::Closure; 22 | pub use data::Data; 23 | pub use inject::Inject; 24 | pub use module::Module; 25 | pub use source::Source; 26 | pub use span::{Span, Spanned}; 27 | -------------------------------------------------------------------------------- /passerine/tests/regressions/milk.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 0.0 4 | 5 | print = x -> magic "print" x 6 | 7 | syntax 'if cond then 'else else { 8 | branch = magic "if" ( 9 | cond, 10 | () -> then, 11 | () -> else, 12 | ) 13 | branch() 14 | } 15 | 16 | bottles = n -> if (n == 0.0) { 17 | n 18 | } else { 19 | print n 20 | println " bottles of milk on the wall!" 21 | print n 22 | println " bottles of milk!" 23 | println "take one down," 24 | println "pass it around," 25 | print (n - 1.0) 26 | println " bottles of milk on the wall!\n" 27 | bottles (n - 1.0) 28 | } 29 | 30 | bottles 3.0 31 | -------------------------------------------------------------------------------- /passerine/tests/regressions/odd_even.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: false 4 | 5 | syntax 'lazy y { 6 | Thunk { () -> y } 7 | } 8 | evaluate = y -> { 9 | Thunk x = y 10 | x () 11 | } 12 | 13 | syntax 'if cond then 'else otherwise { 14 | branch = magic "if" (cond, lazy then, lazy otherwise) 15 | evaluate branch 16 | } 17 | 18 | syntax a 'equals b { 19 | magic "equal" (a, b) 20 | } 21 | 22 | not = n -> if n { false } else { true } 23 | 24 | even = n -> if (n equals 0.0) { 25 | true 26 | } else { 27 | odd (n - 1.0) 28 | } 29 | 30 | odd = n -> if (n equals 0.0) { 31 | false 32 | } else { 33 | even (n - 1.0) 34 | } 35 | 36 | println even 35.0 37 | -------------------------------------------------------------------------------- /passerine/tests/regressions/match.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | 4 | syntax 'if cond then 'else other { 5 | branch = magic "if" ( 6 | cond, 7 | () -> then, 8 | () -> other, 9 | ) 10 | branch () 11 | } 12 | 13 | send = what -> if (what == "friend") { 14 | Cool 2.0 15 | } else { 16 | Ugly (3.0, 4.0) 17 | } 18 | 19 | match = x y -> y 20 | 21 | result = match (send "friend") ( 22 | Cool name -> name + 3.21, 23 | Ugly name -> name + 3.0, 24 | Nice (name, age) -> name + age + 4.0, 25 | ) 26 | 27 | (cool, ugly, nice) = result 28 | 29 | number = Cool 417.48 . cool 30 | 31 | <<<<<<< HEAD 32 | print number 33 | 34 | ======= 35 | println number 36 | >>>>>>> master 37 | -------------------------------------------------------------------------------- /passerine/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passerine" 3 | version = "0.9.2" 4 | authors = [ 5 | "Isaac Clayton (slightknack) ", 6 | "The Passerine Community", 7 | ] 8 | edition = "2021" 9 | description = "A small extensible functional scripting language designed for concise expression with little code." 10 | license = "MIT" 11 | readme = "README.md" 12 | repository = "https://github.com/vrtbl/passerine" 13 | 14 | # We only have development dependencies for testing and profiling 15 | [dependencies] 16 | # I would put this under dev-dependencies if I could. 17 | proptest-derive = "0.3.0" 18 | proptest = "1.0.0" 19 | passerine-common = { path = "../passerine-common" } 20 | passerine-derive = { path = "../passerine-derive" } 21 | -------------------------------------------------------------------------------- /passerine-aspen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "passerine-aspen" 3 | version = "0.5.1" 4 | authors = [ 5 | "Isaac Clayton (slightknack) ", 6 | "The Passerine Community", 7 | ] 8 | edition = "2021" 9 | description = "Passerine's package manager." 10 | license = "MIT" 11 | readme = "README.md" 12 | repository = "https://github.com/vrtbl/aspen" 13 | 14 | [dependencies] 15 | structopt = "0.3" 16 | serde = { version = "1.0", features = ["derive"] } 17 | toml = "0.5" 18 | semver = "0.10" 19 | colored = "2" 20 | 21 | # Make it a path dependency for dev. 22 | passerine = { path = "../passerine" } 23 | passerine-derive = { path = "../passerine-derive" } 24 | 25 | [features] 26 | no_color = ["colored/no-color"] 27 | 28 | [[bin]] 29 | name = "aspen" 30 | path = "src/main.rs" 31 | -------------------------------------------------------------------------------- /passerine-aspen/src/run.rs: -------------------------------------------------------------------------------- 1 | use std::path::PathBuf; 2 | 3 | use passerine::{compile, Source}; 4 | 5 | use crate::{manifest::Manifest, ENTRYPOINT, SOURCE}; 6 | 7 | pub fn run(path: PathBuf) -> Result<(), String> { 8 | // just one file, for now 9 | let (_manifest, path) = Manifest::package(&path)?; 10 | let file = path.join(SOURCE).join(ENTRYPOINT); 11 | 12 | let source = Source::path(&file).map_err(|_| { 13 | format!( 14 | "Could not find source entrypoint '{}/{}'", 15 | SOURCE, ENTRYPOINT 16 | ) 17 | })?; 18 | 19 | let bytecode = compile(source).map_err(|e| e.to_string())?; 20 | 21 | println!("{:#?}", bytecode.lambda); 22 | println!("{}", bytecode.lambda); 23 | 24 | // let mut vm = VM::init(Closure::wrap(bytecode)); 25 | // vm.run().map_err(|e| e.to_string())?; 26 | 27 | Ok(()) 28 | } 29 | -------------------------------------------------------------------------------- /passerine-common/src/effect.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use crate::{data::Data, inject::Inject}; 4 | 5 | // TODO: switch from using From to TryFrom. 6 | 7 | #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] 8 | pub struct EffectId(usize); 9 | 10 | pub struct Handler { 11 | id: EffectId, 12 | _into: PhantomData, 13 | } 14 | 15 | pub struct Effect { 16 | pub id: EffectId, 17 | unmatched_data: Option, 18 | } 19 | 20 | impl Effect { 21 | #[inline(always)] 22 | pub fn matches(&mut self, handler: Handler) -> Option> 23 | where 24 | T: Inject, 25 | { 26 | if self.id == handler.id { 27 | let data = std::mem::replace(&mut self.unmatched_data, None); 28 | data.map(Inject::deserialize) 29 | } else { 30 | None 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /passerine-aspen/src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::{env::current_dir, ffi::OsStr, path::PathBuf}; 2 | 3 | use structopt::StructOpt; 4 | 5 | pub fn package_dir(path: &OsStr) -> PathBuf { 6 | return if path == "." { 7 | current_dir().expect("Can not determine package directory") 8 | } else { 9 | PathBuf::from(path) 10 | }; 11 | } 12 | 13 | #[derive(StructOpt, Debug)] 14 | pub struct Package { 15 | #[structopt(default_value = ".", parse(from_os_str = package_dir))] 16 | pub path: PathBuf, 17 | } 18 | 19 | #[derive(StructOpt, Debug)] 20 | #[structopt(name = "Aspen", bin_name = "aspen", about)] 21 | pub enum Aspen { 22 | /// Creates a new Passerine package 23 | New(Package), 24 | // Update, 25 | // Publish, 26 | /// Runs the specified package 27 | Run(Package), 28 | Repl, 29 | // Test, 30 | // Bench, 31 | // Doc, 32 | // Debug, 33 | } 34 | -------------------------------------------------------------------------------- /passerine/proptest-regressions/compiler/read.txt: -------------------------------------------------------------------------------- 1 | # Seeds for failure cases proptest has generated in the past. It is 2 | # automatically read and these particular cases re-run before any 3 | # novel cases are generated. 4 | # 5 | # It is recommended to check this file in to source control so that 6 | # everyone who runs the test benefits from these saved cases. 7 | cc f28bab0f46258de0811cec0d85c7e372cbeafd5b62c5a19e5fa136f8e4849887 # shrinks to tokens = ParenOpen 8 | cc 7f679b7295fee2637ef6bfa9a2cf4dd9d33afc181701ebc24717191396598747 # shrinks to tokens = [Open(Paren), Close(Curly)] 9 | cc 96f3601918e85087e5099a28fba43e1c8f4b529de0dc59a20bd20df5bdec0c79 # shrinks to tokens = [Open(Paren), Open(Paren), Close(Paren), Open(Paren), Close(Paren), Close(Paren), Close(Paren), Close(Paren), Open(Paren), Close(Paren), Close(Paren), Open(Paren), Open(Paren), Open(Paren), Close(Paren), Open(Paren), Open(Paren), Open(Paren), Open(Paren), Close(Paren), Close(Paren), Close(Paren)] 10 | -------------------------------------------------------------------------------- /passerine-common/src/ty.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | #[derive(Debug, Clone, PartialEq, Eq, Hash)] 4 | pub struct TySymbol(usize); 5 | 6 | /// Built-in Passerine datatypes. 7 | #[derive(Debug, Clone, PartialEq, Eq)] 8 | pub enum Ty { 9 | // Passerine Data (Atomic) 10 | Natural, 11 | Integer, 12 | Float, 13 | // Boolean, // TODO: should be just standard library enum? 14 | String, 15 | 16 | // Function 17 | // -> / 18 | Function { 19 | arg: TySymbol, 20 | body: TySymbol, 21 | effect: TySymbol, 22 | }, 23 | // TODO: fibers still good idea? 24 | Fiber { 25 | takes: TySymbol, 26 | yields: TySymbol, 27 | }, 28 | 29 | // Compound 30 | Tuple(Vec), 31 | List(TySymbol), 32 | // TODO: hashmap? 33 | Record(Vec), // TODO: names for records and enums 34 | Enum(Vec), 35 | } 36 | 37 | pub struct TyPool { 38 | tys: HashMap, 39 | } 40 | -------------------------------------------------------------------------------- /passerine-aspen/src/main.rs: -------------------------------------------------------------------------------- 1 | use structopt::StructOpt; 2 | 3 | // argument parser and configuation 4 | pub mod cli; 5 | pub mod manifest; 6 | pub mod status; 7 | 8 | // command implementations 9 | pub mod add; 10 | pub mod bench; 11 | pub mod debug; 12 | pub mod doc; 13 | pub mod new; 14 | pub mod publish; 15 | pub mod repl; 16 | pub mod run; 17 | pub mod test; 18 | pub mod update; 19 | 20 | use crate::{cli::Aspen, status::Status}; 21 | 22 | // TODO: handle this passerine side 23 | pub const MANIFEST: &str = "aspen.toml"; 24 | pub const SOURCE: &str = "src"; 25 | pub const ENTRYPOINT: &str = "main.pn"; 26 | 27 | fn main() { 28 | let subcommand = Aspen::from_args(); 29 | 30 | let result = match subcommand { 31 | Aspen::New(package) => new::new(package.path), 32 | Aspen::Run(package) => run::run(package.path), 33 | Aspen::Repl => repl::repl(), 34 | _ => unimplemented!(), 35 | }; 36 | 37 | if let Err(r) = result { 38 | Status::fatal().log(&r) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2022 Isaac Clayton and The Passerine Community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /passerine-common/src/code.rs: -------------------------------------------------------------------------------- 1 | #[derive(Default)] 2 | pub struct Store { 3 | data: Vec, 4 | code: Vec, 5 | funs: Vec, 6 | spans: Vec<(usize, Span)>, 7 | } 8 | 9 | struct FunInfo { 10 | offset: usize, 11 | length: usize, 12 | locals: usize, 13 | caps: Vec, 14 | } 15 | 16 | struct FunIndex(usize); 17 | 18 | impl Store { 19 | pub fn empty() -> Store { 20 | Default::default() 21 | } 22 | 23 | pub fn add_data(&mut self, data: Data) -> usize { 24 | let index = self.data.len(); 25 | self.data.append(data); 26 | return index; 27 | } 28 | 29 | pub fn add_function( 30 | &mut self, 31 | locals: usize, 32 | caps: Vec, 33 | bytecode: Vec, 34 | spans: Vec, 35 | ) -> usize { 36 | let offset = self.code.len(); 37 | let length = bytecode.len(); 38 | self.code.push(bytecode); 39 | 40 | let index = self.funs.len(); 41 | self.funs.push(FunInfo { 42 | offset, 43 | length, 44 | locals, 45 | caps, 46 | }); 47 | return index; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /passerine-common/src/closure.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, rc::Rc}; 2 | 3 | use crate::{data::Data, lambda::Lambda}; 4 | 5 | /// Wraps a `Lambda` with some scope context. 6 | /// Each closure is unique when constructed, 7 | /// Because it depends on the surrounding environment it was constructed in. 8 | /// It holds a set of references to variables it captures. 9 | #[derive(Debug, Clone, PartialEq)] 10 | pub struct Closure { 11 | pub lambda: Rc, 12 | pub captures: Vec>>, 13 | } 14 | 15 | impl Closure { 16 | /// Constructs a new `Closure` by wrapping a `Lambda`. 17 | /// This closure has no captured variables when constructed. 18 | pub fn wrap(lambda: Rc) -> Closure { 19 | Closure { 20 | lambda, 21 | captures: vec![], 22 | } 23 | } 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | // use super::*; 29 | // use crate::common::lambda::Lambda; 30 | 31 | // #[test] 32 | // fn unique() { 33 | // let lambda = Lambda::empty(); 34 | // let a = Closure::wrap(lambda.clone()); 35 | // let b = Closure::wrap(lambda.clone()); 36 | // 37 | // assert_ne!(a.id, b.id); 38 | // } 39 | } 40 | -------------------------------------------------------------------------------- /passerine/src/vm/trace.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::common::span::Span; 4 | 5 | /// Represents a runtime error, i.e. a traceback 6 | #[derive(Debug, PartialEq, Eq)] 7 | pub struct Trace { 8 | kind: String, // TODO: enum? 9 | message: String, 10 | spans: Vec, 11 | } 12 | 13 | impl Trace { 14 | /// Creates a new traceback 15 | pub fn error(kind: &str, message: &str, spans: Vec) -> Trace { 16 | Trace { 17 | kind: kind.to_string(), 18 | message: message.to_string(), 19 | spans, 20 | } 21 | } 22 | 23 | /// Used to add context (i.e. function calls) while unwinding the stack. 24 | pub fn add_context(&mut self, span: Span) { 25 | self.spans.push(span); 26 | } 27 | } 28 | 29 | impl fmt::Display for Trace { 30 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 31 | // TODO: better message? 32 | writeln!(f, "Traceback, most recent call last:")?; 33 | 34 | for span in self.spans.iter().rev() { 35 | fmt::Display::fmt(span, f)?; 36 | } 37 | 38 | write!(f, "Runtime {} Error: {}", self.kind, self.message) 39 | } 40 | } 41 | 42 | #[cfg(test)] 43 | mod test { 44 | use std::rc::Rc; 45 | 46 | use super::*; 47 | use crate::common::source::Source; 48 | } 49 | -------------------------------------------------------------------------------- /passerine/tests/regressions/fibonacci.pn: -------------------------------------------------------------------------------- 1 | # action: run 2 | # outcome: success 3 | # expect: 55.0 4 | 5 | # create the syntax for an if statement 6 | syntax 'if condition do 'else otherwise { 7 | # the ffi will return `do` or `otherwise` depending on condition 8 | branch = magic "if" ( 9 | condition, 10 | { () -> do }, 11 | { () -> otherwise }, 12 | ) 13 | 14 | # call the branch 15 | branch () 16 | } 17 | 18 | syntax a 'or b { 19 | magic "if" (a, 20 | true, 21 | b, 22 | ) 23 | } 24 | 25 | syntax 'not a { 26 | magic "if" (a, 27 | false, 28 | true, 29 | ) 30 | } 31 | 32 | syntax a 'equals b { 33 | magic "equal" (a, b) 34 | } 35 | 36 | syntax 'while condition do { 37 | test = () -> condition 38 | 39 | l = () -> { 40 | recurse = magic "if" ( 41 | test (), 42 | { () -> { do; l () } }, 43 | { () -> () }, 44 | ) 45 | recurse () 46 | } 47 | 48 | l () 49 | } 50 | 51 | println_no_line = x -> magic "println" x 52 | 53 | println "starting" 54 | 55 | # a simple recursive fibonacci implementation 56 | fib = n -> { 57 | base = { n equals 0.0 } or { n equals 1.0 } 58 | 59 | if (not base) { 60 | fib (n - 1.0) + fib (n - 2.0) 61 | } else { 62 | 1.0 63 | } 64 | } 65 | 66 | fib 9.0 67 | -------------------------------------------------------------------------------- /passerine/tests/snippets/texpr.pn: -------------------------------------------------------------------------------- 1 | # action: read 2 | # outcome: success 3 | 4 | x = 0 5 | x = 0.0 6 | 7 | x = x -> x + 1 8 | 9 | fn add(names: [String], age: Nat) -> Nat { 10 | for j in 0..100 { 11 | println "Wow, very nice job!" 12 | } 13 | } 14 | 15 | macro define = [signature, ..body] -> { 16 | if let Tree.Group [name, ..args] = signature { 17 | if args::is_empty { args = [()] } 18 | quote { 19 | $name = ..$args -> { 20 | ..($body;) 21 | } 22 | } 23 | |> Result.Ok 24 | } else { 25 | Result.Err "Expected a valid signature" 26 | } 27 | } 28 | 29 | (define (add x y) 30 | (a = x) 31 | (b = y) 32 | (a + b)) 33 | 34 | match list { 35 | [] -> [], 36 | [pivot, ..tail] -> { 37 | lower, higher = [], [] 38 | for item in tail { 39 | if item < pivot { 40 | lower::push item 41 | } else { 42 | higher::push item 43 | } 44 | } 45 | sorted = lower 46 | sorted::push pivot 47 | sorted::append higher 48 | sorted 49 | } 50 | } 51 | 52 | type Option = T => 53 | | Some T 54 | | None 55 | 56 | struct Person { 57 | name: String, 58 | age: Nat, 59 | } 60 | 61 | math = mod { 62 | PI = 3.14 63 | square = x -> x * x 64 | } 65 | 66 | math.square math.PI -------------------------------------------------------------------------------- /passerine-aspen/src/status.rs: -------------------------------------------------------------------------------- 1 | use colored::*; 2 | 3 | pub enum Kind { 4 | Info, 5 | Success, 6 | Warn, 7 | Fatal, 8 | } 9 | 10 | pub struct Status(pub Kind, pub &'static str); 11 | 12 | impl Status { 13 | pub fn info() -> Status { 14 | Status(Kind::Info, "Info") 15 | } 16 | pub fn success() -> Status { 17 | Status(Kind::Success, "Success") 18 | } 19 | pub fn warn() -> Status { 20 | Status(Kind::Warn, "Warning") 21 | } 22 | pub fn fatal() -> Status { 23 | Status(Kind::Fatal, "Fatal") 24 | } 25 | 26 | fn tag(&self) -> ColoredString { 27 | match self.0 { 28 | Kind::Info => self.1.blue(), 29 | Kind::Success => self.1.green(), 30 | Kind::Warn => self.1.yellow(), 31 | Kind::Fatal => self.1.red(), 32 | } 33 | .bold() 34 | } 35 | 36 | fn multiline(&self, lines: Vec<&str>) { 37 | eprint!("\n{} ", self.tag()); 38 | // let blank = " ".repeat(tag.len()).hidden(); 39 | for line in lines { 40 | eprintln!("{}", line); 41 | } 42 | eprintln!() 43 | } 44 | 45 | pub fn log(&self, message: &str) { 46 | let lines = message.lines().collect::>(); 47 | 48 | if lines.len() > 1 { 49 | self.multiline(lines); 50 | } else { 51 | eprintln!("{:>12} {}", self.tag(), message); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /passerine/src/construct/symbol.rs: -------------------------------------------------------------------------------- 1 | use std::hash::Hash; 2 | 3 | // TODO: should SharedSymbol be hash of name or something similar? 4 | 5 | /// Represents a symbol that corresponds to a name. 6 | /// In other words, if two variables have the same name, 7 | /// even if they exist in different scopes, 8 | /// They will have the same [`SharedSymbol`]. 9 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 10 | pub struct SharedSymbol(pub usize); 11 | 12 | /// Represents a unique symbol that corresponds to a single variable. 13 | /// In other words, if two variables with the same name exist in different 14 | /// scopes, They will have different [`UniqueSymbol`]s. 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] 16 | pub struct UniqueSymbol(pub usize); 17 | 18 | // TODO: use VecSet in place of Symbol Table 19 | 20 | /// Represents a set of symbols, whether they be unique by name 21 | /// Or unique by some other measure. 22 | #[derive(Debug)] 23 | pub struct SymbolTable { 24 | // Ordered list of symbols. 25 | // A symbol is in the symbol table if it's inner number is less than lowest 26 | interns: Vec, 27 | } 28 | 29 | impl SymbolTable { 30 | pub fn new() -> SymbolTable { 31 | SymbolTable { interns: vec![] } 32 | } 33 | 34 | pub fn name(&self, unique: &UniqueSymbol) -> SharedSymbol { 35 | return self.interns[unique.0]; 36 | } 37 | 38 | pub fn push(&mut self, shared: SharedSymbol) -> UniqueSymbol { 39 | let index = self.interns.len(); 40 | self.interns.push(shared); 41 | return UniqueSymbol(index); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /passerine-common/src/source.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::Read, 4 | path::{Path, PathBuf}, 5 | rc::Rc, 6 | }; 7 | 8 | // TODO: make path optional 9 | // TODO: represent a hierarchy of sources 10 | 11 | /// `Source` represents some literal source code. 12 | /// Whether a repl session, a file on disk, or some library 13 | /// code. It's essentially a string with a path, the path 14 | /// serving as the source's name. Source files without a 15 | /// path point to `./source`, though this behaviour might 16 | /// change in the future. 17 | #[derive(Debug, PartialEq, Eq)] 18 | pub struct Source { 19 | pub contents: String, 20 | pub path: PathBuf, 21 | } 22 | 23 | impl Source { 24 | /// Creates a new `Source` given both an `&str` and a 25 | /// `PathBuf`. Note that this function does not 26 | /// check that the contents of the file 27 | /// match the source. 28 | /// `Source::path` or `Source::source` should be used 29 | /// instead. 30 | pub fn new(source: &str, path: &Path) -> Rc { 31 | Rc::new(Source { 32 | contents: source.to_string(), 33 | path: path.to_owned(), 34 | }) 35 | } 36 | 37 | /// Build a `Source` from a path. 38 | /// This will read a file to create a new source. 39 | pub fn path(path: &Path) -> std::io::Result> { 40 | let mut source = String::new(); 41 | let mut file = File::open(path)?; 42 | file.read_to_string(&mut source)?; 43 | 44 | Ok(Source::new(&source, path)) 45 | } 46 | 47 | /// Build an empty `Source` containing just a string. 48 | /// Note that this source will point towards `./source`. 49 | pub fn source(source: &str) -> Rc { 50 | Source::new(source, &PathBuf::from("./source")) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /passerine/std/mod.pn: -------------------------------------------------------------------------------- 1 | type Bool = 2 | | True 3 | | False 4 | 5 | use Bool.{..} 6 | 7 | type Option = 8 | T => 9 | | Some T 10 | | None 11 | 12 | use Option.{..} 13 | 14 | type Result = 15 | T E => 16 | | Ok T 17 | | Err E 18 | 19 | compiler = mod { 20 | # TODO: how to link Spans to Sources? 21 | Span = { 22 | source: Nat 23 | offset: Nat 24 | length: Nat 25 | } 26 | 27 | Spanned = T => { 28 | item: T 29 | span: Span 30 | } 31 | 32 | TokenTrees = [Spanned TokenTree] 33 | 34 | TokenTree = 35 | # Grouping 36 | | Block = [Spanned TokenTrees] 37 | | List = TokenTrees 38 | | Form = TokenTrees 39 | # Leafs 40 | | Iden = String 41 | | Label = String 42 | | Op = String 43 | | Lit = Lit 44 | 45 | Lit = 46 | | Nat 47 | | Float 48 | | Integer 49 | | String 50 | | Label = (Nat, Lit) 51 | | () 52 | | Bool 53 | } 54 | 55 | use compiler.{..} 56 | 57 | macro do_twice = token_tree -> { 58 | Spanned { 59 | item: TokenTree.Block [ 60 | token_tree, 61 | token_tree, 62 | ] 63 | span: token_tree.span, 64 | } 65 | 66 | } 67 | 68 | macro quote = token_tree -> { 69 | use TokenTree.{..} 70 | 71 | match token_tree.item 72 | | Block expressions -> {} 73 | | List tree 74 | | Form tree 75 | | Iden iden 76 | | Label label 77 | | Op op -> TokenTree.Group [ 78 | TokenTree.Iden "TokenTree.Op", 79 | TokenTree.Lit (Lit op), 80 | ] 81 | | Lit lit -> TokenTree.Group [ 82 | TokenTree.Iden "TokenTree.Lit", 83 | TokenTree.Lit (Lit lit), 84 | ] 85 | } -------------------------------------------------------------------------------- /passerine-qualm/src/main.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet}; 2 | 3 | mod heap; 4 | pub use heap::{Pointer, Heap}; 5 | 6 | mod stack; 7 | // mod fiber; 8 | mod code; 9 | mod slot; 10 | 11 | pub use slot::Slot; 12 | 13 | // pub struct Code { 14 | // bytes: Vec, 15 | // } 16 | // 17 | // pub struct Worker { 18 | // code_pool: BTreeMap, 19 | // constant_pool: BTreeMap, 20 | // process_pool: BTreeMap, 21 | // } 22 | // 23 | // macro_rules! op { 24 | // { 25 | // fn $name:ident( 26 | // $ip:ident, 27 | // $next_op:ident, 28 | // $stack:ident, 29 | // $heap:ident, 30 | // $code:ident, 31 | // ) $body:expr 32 | // } => { 33 | // pub fn name( 34 | // ip: &mut usize, 35 | // next_op: &mut OpCode, 36 | // stack: &mut Stack, 37 | // heap: &mut Heap, 38 | // code: &Code, 39 | // ) -> std::option::Option { 40 | // body 41 | // } 42 | // }; 43 | // } 44 | // 45 | // op! { 46 | // fn add_u64( 47 | // ip, 48 | // next_op, 49 | // stack, 50 | // heap, 51 | // code, 52 | // ) { 53 | // *next_op = code.prefetch(ip); 54 | // let a = stack.pop(); 55 | // let b = stack.pop(); 56 | // stack.push(a + b); 57 | // None 58 | // } 59 | // 60 | // } 61 | // 62 | // pub fn add_u64( 63 | // ip: &mut usize, 64 | // next_op: &mut OpCode, 65 | // stack: &mut Stack, 66 | // heap: &mut Heap, 67 | // code: &Code, 68 | // ) -> Option { 69 | // *next_op = code.prefetch(ip); 70 | // let a = stack.pop(); 71 | // let b = stack.pop(); 72 | // stack.push(a + b); 73 | // None 74 | // } 75 | 76 | pub fn main() { 77 | todo!(); 78 | } 79 | -------------------------------------------------------------------------------- /passerine/src/vm/slot.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | cell::RefCell, 3 | fmt::{Debug, Formatter, Result}, 4 | rc::Rc, 5 | }; 6 | 7 | use crate::common::{closure::Closure, data::Data}; 8 | 9 | /// Represents a suspended closure. 10 | #[derive(Debug, Clone)] 11 | pub struct Suspend { 12 | pub ip: usize, 13 | pub closure: Closure, 14 | } 15 | 16 | /// Represents the value a slot on the VM can take. 17 | #[derive(Clone)] 18 | pub enum Slot { 19 | // The topmost frame is stored in the VM. 20 | Frame, 21 | // All other frames are suspended. 22 | Suspend(Suspend), 23 | 24 | // Data 25 | Data(Data), 26 | 27 | // Uninitialized Data 28 | NotInit, 29 | 30 | // Refers to a capture stored in the current closure 31 | Ref(Rc>), 32 | } 33 | 34 | impl Slot { 35 | pub fn data(self) -> Data { 36 | match self { 37 | Slot::Data(d) => d, 38 | Slot::Ref(r) => r.borrow().to_owned(), 39 | Slot::Frame | Slot::Suspend(_) | Slot::NotInit => { 40 | unreachable!("expected data on top of stack, found {:?}", self) 41 | } 42 | } 43 | } 44 | 45 | pub fn reference(self) -> Rc> { 46 | match self { 47 | Slot::Data(d) => Rc::new(RefCell::new(d)), 48 | Slot::Ref(r) => r, 49 | Slot::Frame | Slot::Suspend(_) | Slot::NotInit => { 50 | unreachable!("expected reference on top of stack, found {:?}", self) 51 | } 52 | } 53 | } 54 | } 55 | 56 | impl Debug for Slot { 57 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 58 | match self { 59 | Slot::Frame => write!(f, "Frame"), 60 | Slot::Suspend(s) => write!(f, "Suspend({})", s.ip), 61 | Slot::Data(d) => write!(f, "Data({:?})", d), 62 | Slot::Ref(r) => write!(f, "Ref({:?})", r), 63 | Slot::NotInit => write!(f, "NotInit"), 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /passerine-qualm/src/slot.rs: -------------------------------------------------------------------------------- 1 | use std::mem::transmute; 2 | use crate::Pointer; 3 | 4 | #[derive(Debug)] 5 | pub struct Slot(u64); 6 | 7 | impl Slot { 8 | pub unsafe fn zero() -> Slot { 9 | Slot(0) 10 | } 11 | 12 | pub unsafe fn from_bits(bits: u64) -> Slot { 13 | Slot(bits) 14 | } 15 | 16 | pub unsafe fn to_borrowed_pointer(&self) -> Pointer { 17 | Pointer::from_bits(self.0).borrow() 18 | } 19 | 20 | pub unsafe fn to_pointer(&self) -> Pointer { 21 | Pointer::from_bits(self.0) 22 | } 23 | 24 | pub unsafe fn to_u64(&self) -> u64 { 25 | self.0 26 | } 27 | 28 | pub unsafe fn to_i64(&self) -> i64 { 29 | transmute(self.0) 30 | } 31 | 32 | pub unsafe fn to_f64(&self) -> f64 { 33 | transmute(self.0) 34 | } 35 | 36 | /// Interpreting the slot as a bitfield, 37 | /// returns the bit at a given index. 38 | /// If we have the bifield `0b10`, which is 5 decimal: 39 | /// bit at position `0` is `0b0` is `false`, 40 | /// bit at position `1` is `0b1` is `true`, 41 | /// bit at positions `2..=64` are zeroed, so the rest are `false`. 42 | /// 43 | /// # Safety 44 | /// Caller must ensure that the slot in question is a bitfield. 45 | pub unsafe fn to_bool(&self, position: usize) -> bool { 46 | let mask = 1 << position; 47 | mask & self.to_u64() == mask 48 | } 49 | 50 | /// Interpreting the slot as a byte array, 51 | /// returns the byte at a given index. 52 | /// Say we have the usize `0xAAFF`, which is `43775` decimal. 53 | /// Byte at position `0` is `0xFF`, `1` is `0xAA`; 54 | /// the rest of the bits are zeroed, so positions `2..=8` would be `0x00`. 55 | /// # Safety 56 | /// Caller must ensure that the slot in question is a byte. 57 | pub unsafe fn to_byte(&self, position: usize) -> u8 { 58 | let shifted = self.to_u64() >> (position * 8); 59 | (shifted & 0xFF) as u8 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /passerine-aspen/src/repl.rs: -------------------------------------------------------------------------------- 1 | // TODO: preserve values using module system. 2 | 3 | // use std::io::{self, Read}; 4 | // use std::io::Write; 5 | // 6 | // use passerine::{ 7 | // common::{closure::Closure, source::Source, lambda::Lambda}, 8 | // compiler::{lex, parse, desugar, gen, hoist, syntax::Syntax}, 9 | // vm::vm::VM, 10 | // }; 11 | 12 | pub fn repl() -> Result<(), String> { 13 | // println!("Repl is WIP."); 14 | Err("Interactive repl is WIP".into()) 15 | 16 | // println!("Hit ^C to quit.\n"); 17 | 18 | // loop { 19 | // let mut to_eval = String::new(); 20 | // 21 | // let lambda: Result = 'read: loop { 22 | // print!("| "); 23 | // std::io::stdout().flush().unwrap(); 24 | // 25 | // let mut exit = false; 26 | // match io::stdin().read_line(&mut to_eval) { 27 | // Ok(l) if l <= 1 => { exit = true; }, 28 | // _ => () 29 | // } 30 | // 31 | // let source = Source::source(&to_eval); 32 | // let compiled = lex(source) 33 | // .and_then(parse) 34 | // .and_then(desugar) 35 | // .and_then(hoist) 36 | // .and_then(gen); 37 | // 38 | // match compiled { 39 | // Ok(lambda) => { break 'read Ok(lambda); }, 40 | // Err(e) => if exit { break 'read Err(e); }, 41 | // }; 42 | // }; 43 | // 44 | // let lambda = match lambda { 45 | // Ok(l) => l, 46 | // Err(e) => { println!("\n{}\n", e); continue; }, 47 | // }; 48 | // 49 | // let mut vm = VM::init(Closure::wrap(lambda)); 50 | // match vm.run() { 51 | // Ok(()) => { 52 | // let data = vm.stack.pop_data(); 53 | // println!("= {}\n", data) 54 | // }, 55 | // Err(e) => { 56 | // println!("\n{}\n", e); 57 | // }, 58 | // } 59 | // } 60 | } 61 | -------------------------------------------------------------------------------- /passerine-aspen/README.md: -------------------------------------------------------------------------------- 1 | # aspen 2 | Passerine's package manager. 3 | 4 | ## Getting Started 5 | To install the `aspen` command, run this in the shell of your choice: 6 | 7 | ```zsh 8 | bash <(curl -sSf https://www.passerine.io/install.sh) 9 | ``` 10 | 11 | This requires git and a recent version of Cargo to work. 12 | You can inspect the contents of `install.sh` first if you want, 13 | we're not trying to play any tricks on you. 14 | 15 | 2. Start a new Passerine project using Aspen: 16 | ```bash 17 | aspen new example 18 | ``` 19 | 3. This will create a project named `example` in the current directory. 20 | Open this project and look through it in your editor of choice. 21 | 4. To run the project: 22 | ```bash 23 | cd example 24 | aspen run 25 | ``` 26 | 27 | > `aspen run` and most other commands optionally take a path to the project root. 28 | 29 | ### Commands 30 | 31 | > NOTE: Not all commands are implemented ATM. 32 | > Commands in **bold** are partially or wholly implemented. 33 | 34 | | Command | Result | 35 | | ---------- | --------------------------------------------------------- | 36 | | `update` | Updates the Passerine toolchain. | 37 | | **`new`** | Creates a new Passerine package. | 38 | | `publish` | Publishes package to the registries in `Aspen.toml`. | 39 | | `pull` | Pulls fresh packages from the registries in `Aspen.toml`. | 40 | | `add` | Adds a dependency to `Aspen.toml`. | 41 | | **`run`** | Builds and runs the corresponding Passerine package. | 42 | | **`repl`** | Opens a fresh repl session. | 43 | | `test` | Builds and runs the package's tests. | 44 | | `bench` | Builds and runs the package's benchmarks. | 45 | | `doc` | Builds the package's documentation. | 46 | | `debug` | Builds and runs the package in interactive debug mode. | 47 | 48 | An optional path to the project root may be provided. 49 | -------------------------------------------------------------------------------- /passerine-common/src/lit.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | f64, 3 | fmt::{Debug, Display, Formatter, Result}, 4 | }; 5 | 6 | use crate::data::Data; 7 | 8 | pub enum ArbInt { 9 | Small(u128), 10 | Large(Vec), 11 | } 12 | 13 | /// Built-in Passerine datatypes. 14 | #[derive(Debug, Clone, PartialEq, proptest_derive::Arbitrary)] 15 | pub enum Lit { 16 | // TODO: switch to this: 17 | // Number Literals 18 | // Float { 19 | // point: usize, 20 | // mantissa: ArbInt, 21 | // }, 22 | // Integer(ArbInt), 23 | Float(f64), 24 | Integer(i64), 25 | 26 | /// A UTF-8 encoded string. 27 | String(String), 28 | 29 | /// A Label is similar to a type, and wraps some data. 30 | /// in the future labels will have associated namespaces. 31 | #[proptest(skip)] 32 | Label(usize, Box), 33 | 34 | // Compound Datatypes 35 | /// The empty Tuple 36 | Unit, // an empty typle 37 | Boolean(bool), 38 | } 39 | 40 | impl Lit { 41 | pub fn to_data(self) -> Data { 42 | match self { 43 | Lit::Float(f) => Data::Float(f), 44 | Lit::Integer(i) => Data::Integer(i), 45 | Lit::String(s) => Data::String(s), 46 | Lit::Label(_, _) => todo!(), 47 | Lit::Unit => Data::Unit, 48 | Lit::Boolean(b) => Data::Boolean(b), 49 | } 50 | } 51 | } 52 | 53 | impl Display for Lit { 54 | /// Displays some Passerine Data in a pretty manner, as if it were printed 55 | /// to console. 56 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 57 | match self { 58 | Lit::Float(n) => write!(f, "{}", n), 59 | Lit::Integer(n) => write!(f, "{}", n), 60 | Lit::String(s) => write!(f, "{}", s), 61 | // TODO: better representation for Labels 62 | Lit::Label(n, v) => write!(f, "#{}({})", n, v), 63 | Lit::Unit => write!(f, "()"), 64 | Lit::Boolean(b) => { 65 | write!(f, "{}", if *b { "True" } else { "False" }) 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /passerine-aspen/src/new.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::PathBuf}; 2 | 3 | use crate::{ 4 | manifest::Manifest, 5 | status::{Kind, Status}, 6 | ENTRYPOINT, MANIFEST, SOURCE, 7 | }; 8 | 9 | pub fn new(package: PathBuf) -> Result<(), String> { 10 | // get the name of the package 11 | // TODO: allow to be specified via argument 12 | let name = package 13 | .file_name() 14 | .ok_or("Can not determine directory name")? 15 | .to_str() 16 | .ok_or("Directory name is not representable")? 17 | .to_owned(); 18 | 19 | // create the directory 20 | fs::create_dir_all(&package).map_err(|_| "Unable to create package directory")?; 21 | 22 | if package.join(MANIFEST).is_file() { 23 | Status::warn().log(&format!( 24 | "The manifest file ({}) has already been created", 25 | MANIFEST 26 | )) 27 | } else { 28 | // write the manifest 29 | let manifest = Manifest::new(name.clone()); 30 | fs::write( 31 | package.join(MANIFEST), 32 | toml::to_string_pretty(&manifest).map_err(|_| "Could not generate manifest file")?, 33 | ) 34 | .map_err(|_| "Could not write manifest file")?; 35 | } 36 | 37 | if package.join(SOURCE).is_dir() { 38 | Status::warn().log(&format!( 39 | "The source directory ({}/) has already been created", 40 | SOURCE 41 | )) 42 | } else { 43 | // create the source directory 44 | fs::create_dir(package.join(SOURCE)).map_err(|_| "Could not create source directory")?; 45 | } 46 | 47 | if package.join(SOURCE).join(ENTRYPOINT).is_file() { 48 | Status::warn().log(&format!( 49 | "The source entrypoint ({}/{}) has already been created", 50 | SOURCE, ENTRYPOINT 51 | )); 52 | } else { 53 | fs::write( 54 | package.join(SOURCE).join(ENTRYPOINT), 55 | "println \"Hello, Passerine!\"\n", 56 | ) 57 | .map_err(|_| "Could not create source entrypoint")?; 58 | } 59 | 60 | Status(Kind::Success, "Finished") 61 | .log(&format!("The package '{}' was created successfully", name)); 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /passerine-qualm/src/heap/pointer.rs: -------------------------------------------------------------------------------- 1 | /// A tagged copy-on-write pointer to some data in a managed heap. 2 | #[derive(Debug, Clone, Copy)] 3 | pub struct Pointer(u64); 4 | 5 | const OWNED: u64 = 0x8000000000000000; 6 | const POINTER: u64 = 0x7fffffffffffffff; 7 | 8 | impl Pointer { 9 | /// Create an owned pointer from an index. 10 | pub(super) fn new(idx: PointerIdx) -> Pointer { 11 | let idx = idx.to_u64(); 12 | assert!(idx <= POINTER); 13 | Pointer(OWNED | (POINTER & idx)) 14 | } 15 | 16 | /// Pointer arithmetic. 17 | /// Maintains ownership. 18 | pub(super) fn add(&self, slots: u64) -> Pointer { 19 | let new_index: u64 = self.to_idx().to_u64() + slots; 20 | assert!(new_index <= POINTER); 21 | Pointer((self.0 & OWNED) | new_index) 22 | } 23 | 24 | /// Return the internal index of the pointer. 25 | pub(super) fn to_idx(&self) -> PointerIdx { 26 | PointerIdx(self.0 & POINTER) 27 | } 28 | 29 | /// Check whether a reference is borrowing the data it points to. 30 | pub fn is_borrowed(&self) -> bool { 31 | self.0 & OWNED == 0 32 | } 33 | 34 | /// Check whether a reference owns the data it points to. 35 | pub fn is_owned(&self) -> bool { 36 | self.0 & OWNED == OWNED 37 | } 38 | 39 | /// Demote an owned pointer to a borrowed pointer. 40 | pub fn borrow(&self) -> Pointer { 41 | Pointer(self.0 & POINTER) 42 | } 43 | 44 | pub unsafe fn from_bits(bits: u64) -> Pointer { 45 | Pointer(bits) 46 | } 47 | 48 | pub unsafe fn to_bits(self) -> u64 { 49 | self.0 50 | } 51 | } 52 | 53 | /// Used as a key in the maps. 54 | #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Eq, Ord, Hash)] 55 | pub(super) struct PointerIdx(u64); 56 | 57 | impl PointerIdx { 58 | pub(super) fn new(idx: u64) -> PointerIdx { 59 | PointerIdx(idx) 60 | } 61 | 62 | pub(super) fn to_usize(self) -> usize { 63 | self.0 as usize 64 | } 65 | 66 | pub(super) fn to_u64(self) -> u64 { 67 | self.0 68 | } 69 | } 70 | 71 | impl From for PointerIdx { 72 | fn from(pointer: Pointer) -> PointerIdx { 73 | pointer.to_idx() 74 | } 75 | } 76 | 77 | impl std::ops::Add for PointerIdx { 78 | type Output = PointerIdx; 79 | 80 | fn add(self, other: u64) -> PointerIdx { 81 | PointerIdx(self.0 + other) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /passerine-aspen/src/manifest.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Read, path::Path}; 2 | 3 | use semver::Version; 4 | use serde::{Deserialize, Serialize}; 5 | use toml::{self, map::Map}; 6 | 7 | use crate::MANIFEST; 8 | 9 | #[derive(Serialize, Deserialize)] 10 | pub struct Manifest { 11 | package: Package, 12 | dependencies: Map, 13 | } 14 | 15 | #[derive(Serialize, Deserialize)] 16 | pub struct Package { 17 | // required keys 18 | name: String, // package name 19 | version: String, // package version, using semver 20 | authors: Vec, // package authors 21 | 22 | // optional keys 23 | readme: Option, // path to package's readme 24 | license: Option, // Path to package's liscense 25 | repository: Option, // URL to package's repository 26 | documentation: Option, // URL to package's documentation 27 | } 28 | 29 | impl Manifest { 30 | pub fn new(name: String) -> Manifest { 31 | Manifest { 32 | package: Package { 33 | name, 34 | version: format!("{}", Version::new(0, 0, 0)), 35 | authors: vec![], 36 | readme: None, 37 | license: None, 38 | repository: None, 39 | documentation: None, 40 | }, 41 | dependencies: Map::new(), 42 | } 43 | } 44 | 45 | pub fn package(mut path: &Path) -> Result<(Manifest, &Path), String> { 46 | let mut source = String::new(); 47 | let mut file = None; 48 | 49 | // search up path for manifest 50 | while let None = file { 51 | match File::open(path.join(MANIFEST)) { 52 | Err(_) => { 53 | path = path 54 | .parent() 55 | .ok_or("The manifest file could not be found")?; 56 | } 57 | Ok(f) => { 58 | file = Some(f); 59 | } 60 | }; 61 | } 62 | 63 | file.unwrap() 64 | .read_to_string(&mut source) 65 | .map_err(|_| "The manifest file could not be read")?; 66 | 67 | return Ok(( 68 | Manifest::parse(&source).ok_or("Could not parse the manifest file")?, 69 | path, 70 | )); 71 | } 72 | 73 | pub fn parse(source: &str) -> Option { 74 | // TODO: error handling 75 | toml::from_str(source).ok() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /passerine-common/src/inject.rs: -------------------------------------------------------------------------------- 1 | use crate::data::Data; 2 | 3 | /// Indicates that a Rust data structure can be serialized to Passerine data, 4 | /// and that passerine data can be deserialized into that datastructure. 5 | /// The conversion should be round-trip such that `T == 6 | /// deserialize(serialize(T))`. 7 | pub trait Inject 8 | where 9 | Self: Sized, 10 | { 11 | /// An infallible conversion from self to `Data`. 12 | fn serialize(item: Self) -> Data; 13 | /// A potentially fallible conversion (due to malformed `Data`) that creates 14 | /// `Self` by deserializing some `Data`. 15 | fn deserialize(data: Data) -> Option; 16 | } 17 | 18 | macro_rules! impl_inject { 19 | ($type:ty where $data:ident => $from:expr, $item:ident => $into:expr,) => { 20 | // With the above two implemented, 21 | // we can implement inject automatically. 22 | impl Inject for $type { 23 | fn serialize($item: Self) -> Data { 24 | $into 25 | } 26 | fn deserialize($data: Data) -> Option { 27 | $from 28 | } 29 | } 30 | }; 31 | } 32 | 33 | // Data type 34 | 35 | impl_inject! { 36 | Data where 37 | from => Some(from), 38 | into => into, 39 | } 40 | 41 | // Unit type 42 | 43 | impl_inject! { 44 | () where 45 | from => { assert_eq!(from, Data::Unit); Some(()) }, 46 | _into => Data::Unit, 47 | } 48 | 49 | // Floats 50 | 51 | impl_inject! { 52 | f64 where 53 | from => match from { 54 | Data::Float(f) => Some(f), 55 | _ => None, 56 | }, 57 | into => Data::Float(into), 58 | } 59 | 60 | // Integers 61 | 62 | impl_inject! { 63 | i64 where 64 | from => match from { 65 | Data::Integer(i) => Some(i), 66 | _ => None, 67 | }, 68 | into => Data::Integer(into), 69 | } 70 | 71 | // Booleans 72 | 73 | impl_inject! { 74 | bool where 75 | from => match from { 76 | Data::Boolean(b) => Some(b), 77 | _ => None, 78 | }, 79 | into => Data::Boolean(into), 80 | } 81 | 82 | // Strings 83 | 84 | impl_inject! { 85 | String where 86 | from => match from { 87 | Data::String(s) => Some(s), 88 | _ => None, 89 | }, 90 | into => Data::String(into), 91 | } 92 | 93 | // Tuples 94 | 95 | // impl_inject! { 96 | // Vec where 97 | // from => match from { 98 | // Data::Tuple(t) => t.to_owned(), 99 | // _ => panic!(), 100 | // }, 101 | // into => Data::Tuple(into), 102 | // } 103 | -------------------------------------------------------------------------------- /passerine/src/construct/scope.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fmt::Debug, hash::Hash}; 2 | 3 | use crate::construct::symbol::UniqueSymbol; 4 | 5 | /// Represents an ordered set of elements with O(1) membership checking. 6 | /// Note that this is insert-only. 7 | /// Should be treated like an allocation pool. 8 | #[derive(Clone, PartialEq)] 9 | pub struct VecSet { 10 | order: Vec, 11 | members: HashMap, 12 | } 13 | 14 | impl Debug for VecSet 15 | where 16 | T: Eq + Hash + Clone + Debug, 17 | { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | write!(f, "{:?}", self.items()) 20 | } 21 | } 22 | 23 | impl VecSet { 24 | pub fn new() -> Self { 25 | VecSet { 26 | order: vec![], 27 | members: HashMap::new(), 28 | } 29 | } 30 | 31 | /// Push a member onto the Vec. 32 | /// Does not check if the member already exists in the Vec. 33 | pub fn push(&mut self, item: T) { 34 | if !self.contains(&item) { 35 | self.members.insert(item.clone(), self.order.len()); 36 | self.order.push(item); 37 | } 38 | } 39 | 40 | pub fn contains(&self, item: &T) -> bool { 41 | self.members.contains_key(item) 42 | } 43 | 44 | pub fn index_of(&self, item: &T) -> Option { 45 | self.members.get(item).copied() 46 | } 47 | 48 | // Marks an item as removed. Does not actually remove the item to preserve 49 | // indexes in hash map. Returns none if the item existed and was removed. 50 | pub fn remove(&mut self, item: &T) -> bool { 51 | self.members.remove(item).is_some() 52 | } 53 | 54 | // TODO: this function needs to be DELETED: 55 | 56 | pub fn items(&self) -> Vec { 57 | self.order 58 | .iter() 59 | .filter(|x| self.contains(x)) 60 | .cloned() 61 | .collect() 62 | } 63 | 64 | pub fn len(&self) -> usize { 65 | self.members.len() 66 | } 67 | } 68 | 69 | #[derive(Debug, Clone, PartialEq)] 70 | pub struct Scope { 71 | pub locals: VecSet, 72 | pub nonlocals: VecSet, 73 | } 74 | 75 | impl Scope { 76 | pub fn new() -> Scope { 77 | Scope { 78 | locals: VecSet::new(), 79 | nonlocals: VecSet::new(), 80 | } 81 | } 82 | 83 | pub fn is_local(&self, unique_symbol: UniqueSymbol) -> bool { 84 | self.locals.contains(&unique_symbol) 85 | } 86 | 87 | pub fn is_nonlocal(&self, unique_symbol: UniqueSymbol) -> bool { 88 | self.nonlocals.contains(&unique_symbol) 89 | } 90 | 91 | pub fn local_index(&self, unique_symbol: UniqueSymbol) -> Option { 92 | self.locals.index_of(&unique_symbol) 93 | } 94 | 95 | pub fn nonlocal_index(&self, unique_symbol: UniqueSymbol) -> Option { 96 | self.nonlocals.index_of(&unique_symbol) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /passerine-common/src/number.rs: -------------------------------------------------------------------------------- 1 | /// Splits a number into a vector of bytes. 2 | /// Each byte in the vector is layed out as follows: 3 | /// ```plain 4 | /// CDDDDDDD 5 | /// ``` 6 | /// Where C is the continue bit-flag, and d are data bits. 7 | /// These sequences are designed to be insertable into byte streams. 8 | /// For example, `0b10001000_00100101_00100100` would become 9 | /// `[0b10100100, 0b11001010, 0b10100000, 0b00001000]`. 10 | pub fn split_number(n: usize) -> Vec { 11 | // high bit = last byte in chain? 12 | // low bits = binary representation 13 | let mut bytes = vec![]; 14 | let mut i = n; 15 | let chunk = 0b1000_0000; 16 | 17 | loop { 18 | let low = i % chunk; // get next 7 bits in usize 19 | i /= chunk; // shift right by 7 20 | 21 | // set high bit & append to chain 22 | bytes.push(if bytes.is_empty() { chunk + low } else { low } as u8); 23 | 24 | // like a do-while 25 | // makes sure a number is always pushed 26 | if i == 0 { 27 | break; 28 | } 29 | } 30 | 31 | // reverse chain so high bit byte is last 32 | bytes.reverse(); 33 | return bytes; 34 | } 35 | 36 | /// This takes a stream of bytes, and builds the next number in it. 37 | /// Note that this function tries to build a number no matter what, 38 | /// even if the byte stream does not have a number, is empty, or ends after a 39 | /// continue bit is set. 40 | pub fn build_number(bytes: &[u8]) -> (usize, usize) /* (index, eaten) */ { 41 | let mut i: usize = 0; 42 | let mut e = 0; 43 | let chunk = 0b1000_0000; 44 | 45 | for byte in bytes { 46 | // shift left by 7 47 | e += 1; 48 | i *= chunk as usize; 49 | 50 | // check if this byte is the last byte in the sequence 51 | // you pass remaining bytecode, so early breaking is important 52 | if byte >= &chunk { 53 | i += (byte - chunk) as usize; 54 | break; 55 | } else { 56 | i += *byte as usize; 57 | } 58 | } 59 | 60 | return (i, e); 61 | } 62 | 63 | #[cfg(test)] 64 | mod test { 65 | use super::*; 66 | 67 | #[test] 68 | fn encode_decode() { 69 | // big number 70 | let x = 7_289_529_732_981_739_357; 71 | assert_eq!(build_number(&split_number(x)), (x, 9)); 72 | } 73 | 74 | #[test] 75 | fn rollover() { 76 | let x = 256; 77 | assert_eq!(split_number(x), vec![0b0000_0010, 0b1000_0000]) 78 | } 79 | 80 | #[test] 81 | fn extra_bytes() { 82 | // encode number 83 | let x = 42069; 84 | let bytes = split_number(x); 85 | let eat = bytes.len(); 86 | 87 | // add junk data to end 88 | let mut extra = bytes.clone(); 89 | extra.append(&mut vec![0xBA, 0xDA, 0x55]); 90 | 91 | assert_eq!((x, eat), build_number(&bytes)); 92 | assert_eq!((x, eat), build_number(&extra)); 93 | } 94 | 95 | #[test] 96 | fn zero() { 97 | let mut zero = split_number(0); 98 | zero.push(2); // will most likely be 2 if split/build_number doesn't work 99 | assert_eq!(build_number(&zero), (0, 1)); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /passerine-common/src/module.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::Path, rc::Rc}; 2 | 3 | use crate::source::Source; 4 | 5 | pub struct Module { 6 | source: Rc, 7 | children: Vec, 8 | } 9 | 10 | pub const ENTRY_POINT: &str = "main"; 11 | pub const EXTENSION: &str = "pn"; 12 | 13 | // TODO: handle symlinks, ugh 14 | 15 | impl Module { 16 | pub fn new_from_dir(entry_path: &Path) -> Result { 17 | // grab the entries in the directory 18 | let paths = fs::read_dir(entry_path).map_err(|_| { 19 | format!( 20 | "The path `{}` could not be read as a directory", 21 | entry_path.display() 22 | ) 23 | })?; 24 | 25 | // incrementally build up the struct 26 | let entry_point_name = format!("{}.{}", ENTRY_POINT, EXTENSION); 27 | let mut entry: Option> = None; 28 | let mut children = vec![]; 29 | 30 | for path in paths { 31 | // get an actual entry 32 | let path = path 33 | .map_err(|_| { 34 | format!( 35 | "Err while scanning the module located at `{}`", 36 | entry_path.display() 37 | ) 38 | })? 39 | .path(); 40 | 41 | // classify the entry 42 | let is_source_file = path.extension().map(|x| x == EXTENSION).unwrap_or(false); 43 | let is_entry_point = path.file_stem().map(|x| x == ENTRY_POINT).unwrap_or(false); 44 | 45 | // grab the module at the given path 46 | let module = if path.is_dir() { 47 | if path.join(&entry_point_name).exists() { 48 | Module::new_from_dir(&path)? 49 | } else { 50 | continue; 51 | } 52 | } else if path.is_file() && is_source_file { 53 | let source = Source::path(&path) 54 | .map_err(|_| format!("Could not read source file `{}`", path.display()))?; 55 | 56 | if is_entry_point { 57 | if let Some(other_path) = entry { 58 | return Err(format!( 59 | "Two potential entry points (`{}` and `{}`) for a single module", 60 | other_path.path.display(), 61 | path.display(), 62 | )); 63 | } else { 64 | entry = Some(source); 65 | } 66 | continue; 67 | } else { 68 | Module { 69 | source, 70 | children: Vec::new(), 71 | } 72 | } 73 | } else { 74 | continue; 75 | }; 76 | 77 | // append the module to the list of child modules 78 | children.push(module); 79 | } 80 | 81 | let source = entry.ok_or_else(|| { 82 | format!( 83 | "No entry point (e.g. `{}`) in the directory for the module `{}`", 84 | entry_point_name, 85 | entry_path.display() 86 | ) 87 | })?; 88 | 89 | Ok(Module { source, children }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /passerine/src/compiler/expand.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::rc::Rc; 3 | 4 | use crate::common::{lambda::Lambda, Spanned}; 5 | use crate::compiler::{compile_token_tree, Syntax}; 6 | use crate::construct::token::{Token, TokenTree}; 7 | 8 | pub struct Expander { 9 | rules: HashMap>, 10 | } 11 | 12 | impl Expander { 13 | pub fn expand( 14 | token_tree: Spanned, 15 | mut rules: HashMap>, 16 | ) -> Result, Syntax> { 17 | let mut expander = Expander { 18 | rules: HashMap::new(), 19 | }; 20 | 21 | for (name, rule) in rules.drain() { 22 | let bytecode = compile_token_tree(rule)?; 23 | expander.rules.insert(name, bytecode); 24 | } 25 | 26 | expander.walk(token_tree) 27 | } 28 | 29 | pub fn walk( 30 | &self, 31 | token_tree: Spanned, 32 | ) -> Result, Syntax> { 33 | let Spanned { 34 | item: token_tree, 35 | span, 36 | } = token_tree; 37 | 38 | let result = match token_tree { 39 | TokenTree::Form(form) => self.expand_form(form)?, 40 | 41 | // trivial conversion 42 | TokenTree::Block(block) => { 43 | let mut new_block = Vec::new(); 44 | for trees in block { 45 | let mut new_trees = Vec::new(); 46 | let Spanned { item, span } = trees; 47 | for tree in item.into_iter() { 48 | new_trees.push(self.walk(tree)?) 49 | } 50 | new_block.push(Spanned::new(new_trees, span)); 51 | } 52 | TokenTree::Block(new_block) 53 | }, 54 | TokenTree::List(trees) => TokenTree::List( 55 | trees 56 | .into_iter() 57 | .map(|tree| self.walk(tree)) 58 | .collect::, _>>()?, 59 | ), 60 | // trivial conversion 61 | TokenTree::Iden(iden) => TokenTree::Iden(iden), 62 | TokenTree::Label(label) => TokenTree::Label(label), 63 | TokenTree::Op(op) => TokenTree::Op(op), 64 | TokenTree::Lit(lit) => TokenTree::Lit(lit), 65 | }; 66 | 67 | Ok(Spanned::new(result, span)) 68 | } 69 | 70 | pub fn walk_form( 71 | &self, 72 | form: Vec>, 73 | ) -> Result { 74 | Ok(TokenTree::Form( 75 | form.into_iter() 76 | .map(|tree| self.walk(tree)) 77 | .collect::, _>>()?, 78 | )) 79 | } 80 | 81 | pub fn expand_form( 82 | &self, 83 | mut form: Vec>, 84 | ) -> Result { 85 | assert!(form.len() >= 2); 86 | let first = form.remove(0); 87 | 88 | if let TokenTree::Iden(iden) = first.item { 89 | if let Some(rule) = self.rules.get(&iden) { 90 | // TODO: call compiled function somehow 91 | let expanded = todo!(); 92 | return expanded; 93 | } 94 | } 95 | 96 | self.walk_form(form) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /passerine-common/src/opcode.rs: -------------------------------------------------------------------------------- 1 | /// This enum represents a single opcode. 2 | /// Under the hood, it's just a byte. 3 | /// This allows non opcode bytes to be inserted in bytecode streams. 4 | #[repr(u8)] 5 | #[derive(Debug, PartialEq, Eq)] 6 | pub enum Opcode { 7 | /// Load a constant. 8 | Con = 0, 9 | /// Load uninitialized Data. 10 | NotInit = 1, 11 | /// Delete a value off the stack. 12 | Del = 2, 13 | /// Calls out to a Rust function via FFI 14 | FFICall = 3, 15 | /// Copies topmost value on the stack. 16 | Copy = 4, 17 | /// Moves a variable onto the heap. 18 | Capture = 5, 19 | /// Save a constant into a variable. 20 | Save = 6, 21 | /// Save a value to a captured variable. 22 | SaveCap = 7, 23 | /// Push a copy of a variable onto the stack. 24 | Load = 8, 25 | /// Load a copy of a captured variable. 26 | LoadCap = 9, 27 | /// Call a function. 28 | Call = 10, 29 | /// Return from a function. 30 | Return = 11, 31 | /// Creates a closure over the current local environment. 32 | Closure = 12, 33 | /// Prints a value. 34 | Print = 13, 35 | /// 36 | Handler = 14, 37 | /// 38 | Effect = 15, 39 | /// Constructs a label. 40 | Label = 16, 41 | /// Constructs a tuple. 42 | Tuple = 17, 43 | /// 44 | Record = 18, 45 | /// Destructures atomic data by asserting it matches exactly. 46 | UnData = 19, 47 | // TODO: make unlabel take the label index as an arg. 48 | /// Destructures a label. 49 | UnLabel = 20, 50 | /// Destructures a tuple. 51 | UnTuple = 21, 52 | /// Add two numbers on the stack. 53 | Add = 22, 54 | /// Subtract two numbers on the stack. 55 | Sub = 23, 56 | /// Negate a number. 57 | Neg = 24, 58 | /// Multiple two numbers on the stack. 59 | Mul = 25, 60 | /// Divide two numbers, raising ZeroDiv side effect 61 | Div = 26, 62 | /// Take the remainder of two numbers, raising ZeroDiv side effect 63 | Rem = 27, 64 | /// Take a number to a power. 65 | Pow = 28, 66 | /// Does nothing. Must always be last. 67 | Noop = 29, 68 | } 69 | 70 | impl Opcode { 71 | /// Convert a raw byte to an opcode. 72 | /// Note that non-opcode bytes should never be interpreted as an opcode. 73 | /// Under the hood, this is just a transmute, so the regular cautions apply. 74 | /// This *should* never cause a crash 75 | /// and if it does, the vm's designed to crash hard 76 | /// so it'll be pretty obvious. 77 | pub fn from_byte(byte: u8) -> Opcode { 78 | unsafe { std::mem::transmute(byte) } 79 | } 80 | 81 | /// Convert a raw byte to an opcode. 82 | /// Performing a bounds check first. 83 | /// Used for bytecode verification, 84 | /// Do not use in a hot loop. 85 | pub fn from_byte_safe(byte: u8) -> Option { 86 | // safety: we do a bounds check on the byte 87 | if byte <= Opcode::Noop as u8 { 88 | Some(Opcode::from_byte(byte)) 89 | } else { 90 | None 91 | } 92 | } 93 | } 94 | 95 | #[cfg(test)] 96 | mod tests { 97 | use super::*; 98 | 99 | #[test] 100 | fn safe() { 101 | assert_eq!(None, Opcode::from_byte_safe((Opcode::Noop as u8) + 1)); 102 | assert_eq!( 103 | Some(Opcode::Noop), 104 | Opcode::from_byte_safe(Opcode::Noop as u8) 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /passerine/src/compiler/desugar.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | common::{Span, Spanned}, 3 | construct::{ 4 | symbol::SharedSymbol, 5 | tree::{Base, Lambda, Sugar, AST, CST}, 6 | }, 7 | }; 8 | 9 | pub struct Desugarer; 10 | 11 | type SharedBase = Base, SharedSymbol>; 12 | 13 | impl Desugarer { 14 | // TODO: just rename walk to desugar? 15 | // TODO: is this pass infallible? 16 | // Maybe make Result once more things are added... 17 | pub fn desugar(ast: Spanned) -> Spanned { 18 | Desugarer::walk(ast) 19 | } 20 | 21 | fn walk(ast: Spanned) -> Spanned { 22 | // TODO: use this destructuring pattern throughout codebase! 23 | let Spanned { item, span } = ast; 24 | let item = match item { 25 | AST::Base(b) => CST::Base(Desugarer::walk_base(b)), 26 | AST::Lambda(l) => CST::Lambda(Desugarer::walk_lambda(l)), 27 | AST::Sugar(s) => Desugarer::walk_sugar(s), 28 | }; 29 | return Spanned::new(item, span); 30 | } 31 | 32 | fn walk_base(b: SharedBase) -> SharedBase { 33 | match b { 34 | Base::Symbol(s) => Base::Symbol(s), 35 | Base::Label(l) => Base::Label(l), 36 | Base::Lit(l) => Base::Lit(l), 37 | Base::Tuple(t) => Base::Tuple(t.into_iter().map(Desugarer::walk).collect()), 38 | Base::Module(m) => Base::module(Desugarer::walk(*m)), 39 | Base::Block(b) => Base::Block(b.into_iter().map(Desugarer::walk).collect()), 40 | Base::Call(f, a) => Base::call(Desugarer::walk(*f), Desugarer::walk(*a)), 41 | Base::Assign(p, e) => Base::assign(p, Desugarer::walk(*e)), 42 | Base::Effect(_) => todo!("need to handle effects"), 43 | } 44 | } 45 | 46 | fn walk_lambda(l: Lambda>) -> Lambda> { 47 | let Lambda { arg, body } = l; 48 | let body = Desugarer::walk(*body); 49 | return Lambda::new(arg, body); 50 | } 51 | 52 | fn walk_sugar(s: Sugar>) -> CST { 53 | match s { 54 | Sugar::Group(g) => Desugarer::walk(*g).item, 55 | // TODO: just do this during parsing haha 56 | // turn a form into a call: 57 | Sugar::Form(f) => { 58 | // we know the form can not be empty... 59 | // and must have at least two items... 60 | assert!(f.len() >= 2); 61 | let mut form_items = f.into_iter(); 62 | let mut fun = Desugarer::walk(form_items.next().unwrap()); 63 | 64 | for arg in form_items { 65 | let arg = Desugarer::walk(arg); 66 | let span = Span::combine(&fun.span, &arg.span); 67 | let call = SharedBase::call(fun, arg); 68 | fun = Spanned::new(CST::Base(call), span); 69 | } 70 | 71 | fun.item 72 | } 73 | // TODO: don't ignore type annotations! 74 | Sugar::Is(e, _) => { 75 | unimplemented!( 76 | "type annotations will be implemented when the type checker is implemented" 77 | ) 78 | } 79 | Sugar::Comp(arg, fun) => { 80 | CST::Base(Base::call(Desugarer::walk(*fun), Desugarer::walk(*arg))) 81 | } 82 | Sugar::Field(_, _) => { 83 | unimplemented!("field access will be implemented when structs are implemented") 84 | } 85 | Sugar::Keyword(_) => todo!(), 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /passerine/src/compiler/mod.rs: -------------------------------------------------------------------------------- 1 | //! This module contains the compiler implementation. 2 | //! This module also implements 3 | //! 4 | //! Note that more steps (e.g. ones applying typechecking 5 | //! operations, optimization passes, etc.) 6 | //! may be implemented in the future. 7 | 8 | pub mod lex; 9 | pub use lex::Lexer; 10 | 11 | pub mod read; 12 | pub use read::Reader; 13 | 14 | // pub mod expand; 15 | // pub use expand::Expander; 16 | 17 | pub mod parse; 18 | pub use parse::Parser; 19 | 20 | pub mod desugar; 21 | pub use desugar::Desugarer; 22 | 23 | pub mod hoist; 24 | pub use hoist::Hoister; 25 | 26 | // pub mod unify; 27 | 28 | pub mod compile; 29 | pub use compile::Compiler; 30 | 31 | pub mod syntax; 32 | pub use syntax::Syntax; 33 | 34 | use std::{collections::HashMap, rc::Rc}; 35 | 36 | use crate::{ 37 | common::{lambda::Lambda, Source, Spanned}, 38 | construct::{ 39 | scope::Scope, 40 | symbol::SharedSymbol, 41 | token::{TokenTree, Tokens}, 42 | tree::{AST, CST, SST}, 43 | }, 44 | }; 45 | 46 | #[inline(always)] 47 | pub fn lex(source: Rc) -> Result, Syntax> { 48 | Lexer::lex(source) 49 | } 50 | 51 | #[inline(always)] 52 | pub fn read(source: Rc) -> Result, Syntax> { 53 | let tokens = lex(source)?; 54 | Reader::read(tokens) 55 | } 56 | 57 | #[inline(always)] 58 | pub fn parse(source: Rc) -> Result<(Spanned, HashMap), Syntax> { 59 | let token_tree = read(source)?; 60 | Parser::parse(token_tree) 61 | } 62 | 63 | #[inline(always)] 64 | pub fn desugar( 65 | source: Rc, 66 | ) -> Result<(Spanned, HashMap), Syntax> { 67 | let (ast, symbols) = parse(source)?; 68 | Ok((Desugarer::desugar(ast), symbols)) 69 | } 70 | 71 | #[inline(always)] 72 | pub fn hoist(source: Rc) -> Result<(Spanned, Scope), Syntax> { 73 | let (cst, symbols) = desugar(source)?; 74 | Hoister::hoist(cst, symbols) 75 | } 76 | 77 | #[inline(always)] 78 | pub fn gen(source: Rc) -> Result, Syntax> { 79 | let (sst, scope) = hoist(source)?; 80 | Compiler::compile(sst, scope) 81 | } 82 | 83 | #[inline(always)] 84 | pub fn compile_sst(sst: Spanned, scope: Scope) -> Result, Syntax> { 85 | Compiler::compile(sst, scope) 86 | } 87 | 88 | // TODO: convert symbols to type alias somewhere 89 | #[inline(always)] 90 | pub fn compile_cst( 91 | cst: Spanned, 92 | symbols: HashMap, 93 | ) -> Result, Syntax> { 94 | let (sst, scope) = Hoister::hoist(cst, symbols)?; 95 | compile_sst(sst, scope) 96 | } 97 | 98 | #[inline(always)] 99 | pub fn compile_ast( 100 | ast: Spanned, 101 | symbols: HashMap, 102 | ) -> Result, Syntax> { 103 | let cst = Desugarer::desugar(ast); 104 | compile_cst(cst, symbols) 105 | } 106 | 107 | #[inline(always)] 108 | pub fn compile_token_tree(token_tree: Spanned) -> Result, Syntax> { 109 | let (ast, symbols) = Parser::parse(token_tree)?; 110 | compile_ast(ast, symbols) 111 | } 112 | 113 | #[inline(always)] 114 | pub fn compile_tokens(tokens: Spanned) -> Result, Syntax> { 115 | let token_tree = Reader::read(tokens)?; 116 | compile_token_tree(token_tree) 117 | } 118 | 119 | #[inline(always)] 120 | pub fn compile_source(source: Rc) -> Result, Syntax> { 121 | let tokens = Lexer::lex(source)?; 122 | compile_tokens(tokens) 123 | } 124 | -------------------------------------------------------------------------------- /passerine/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Passerine 2 | If you're reading this, thanks for taking a look! If you would like to contribute to Passerine, we have some simple rules to follow. 3 | 4 | ## Code of Conduct 5 | If you find any bugs or would like to request any features, please open an issue. Once the issue is open, feel free to put in a pull request to fix the issue. Small corrections, like fixing spelling errors, or updating broken urls are appreciated. All pull requests must have adequate testing and review to assure proper function before being accepted. 6 | 7 | We encourage an open, friendly, and supportive environment around the development of Passerine. If you disagree with someone for any reason, discuss the issue and express you opinions, don't attack the person. Discrimination of any kind against any person is not permitted. If you detract from this project's collaborative environment, you'll be prevented from participating in the future development of this project until you prove you can behave yourself adequately. Please use sound reasoning to support your suggestions - don't rely on arguments based on 'years of experience,' supposed skill, job title, etc. to get your points across. 8 | 9 | # General Guidelines 10 | Readable code with clear behavior works better than illegible optimized code. For things that are very performance-oriented, annotations describing what, how, and why are essential. 11 | 12 | Each commit should denote exactly one thing, whether it be a bug fix, or a feature addition. Try not to do both at the same time - it makes code harder to review. Once the codebase is stable, new features should be: 13 | 14 | 1. First opened as an issue and discussed. 15 | 2. Forked and developed in a new branch. 16 | 3. Put in as a pull request. 17 | 4. Tested and reviewed for bugs, which are fixed. 18 | 5. If everything looks good, it will then be merged. 19 | 20 | After a while, we plan to implement some sort of RFC process. But, given the small toy-ish status of Passerine, this is unlikely to happen without much support. 21 | 22 | Each feature will be given a minor release, which should be tagged. If Passerine garners more popularity, we'll move towards a nightly + rolling release beta. We're also about at the stage where we're looking for core team members. If you're interested, please contribute. When you write well-written long-lasting code (read: lines of code in current release ✕ how long each line has been there), and demonstrate an open, can-do attitude, we'll reach out to you. 23 | 24 | # Integration Tests 25 | If you notice any unsound behavior, like an internal compile error or an incorrect, 26 | 27 | 1. Reduce the behavior to the minimum required amount of passerine code that causes that error 28 | 2. Open an issue explaining what led to the error, what you expected to happen, and the minimum reproducible example. 29 | 3. Optionally, add the snippet to `tests/snippets` and test that the test fails (by running `cargo test snippets`). 30 | 31 | ## What is a test snippet? 32 | A test snippet is some Passerine code that tests a specific outcome. Here's a simple test snippet: 33 | 34 | ```passerine 35 | -- action: run 36 | -- outcome: success 37 | -- expect: "Banana" 38 | 39 | print "Hello, World!" 40 | 41 | "Banana" 42 | ``` 43 | 44 | A test snippet starts with a series of comments, each one forming a key-value pair. The `action` is what the compiler should do with the snippet: 45 | 46 | - `lex` 47 | - `parse` 48 | - `desugar` 49 | - `hoist` 50 | - `compile` 51 | - `run` 52 | 53 | The outcome specifies the specific result: 54 | 55 | - No errors are raised: `success` 56 | - A syntax error is raised: `syntax` 57 | - A runtime error is raised: `trace` 58 | 59 | Optionally, if the action is `run` an `outcome` may be specified. This treats the snippet like a function body, and compares the returned value with the expected value. 60 | 61 | Whenever you add a feature, add snippet tests that demonstrate how this feature should (and should not) work. 62 | -------------------------------------------------------------------------------- /passerine/src/construct/token.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | use crate::common::{lit::Lit, span::Spanned}; 4 | 5 | #[derive(Debug, Clone, PartialEq, proptest_derive::Arbitrary)] 6 | pub enum Delim { 7 | Paren, 8 | Curly, 9 | Square, 10 | } 11 | 12 | impl Display for Delim { 13 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 14 | let name = match self { 15 | Delim::Paren => "parenthesis", 16 | Delim::Curly => "curly brackets", 17 | Delim::Square => "square brackets", 18 | }; 19 | 20 | write!(f, "{}", name) 21 | } 22 | } 23 | 24 | pub type Tokens = Vec>; 25 | 26 | #[derive(Debug, Clone, PartialEq, proptest_derive::Arbitrary)] 27 | pub enum Token { 28 | // Grouping 29 | Open(Delim), 30 | Close(Delim), 31 | Sep, 32 | 33 | // Leafs 34 | Iden(String), 35 | Label(String), 36 | Op(String), 37 | Lit(Lit), 38 | } 39 | 40 | pub type TokenTrees = Vec>; 41 | 42 | /// These are the different tokens the lexer will output. 43 | /// `Token`s with data contain that data, 44 | /// e.g. a boolean will be a `Lit::Boolean(...)`, not just a string. 45 | /// `Token`s can be spanned using `Spanned`. 46 | #[derive(Debug, Clone, PartialEq)] 47 | pub enum TokenTree { 48 | // Grouping 49 | Block(Vec>), 50 | List(TokenTrees), 51 | Form(TokenTrees), 52 | 53 | // Leafs 54 | Iden(String), 55 | Label(String), 56 | Op(String), 57 | Lit(Lit), 58 | } 59 | 60 | impl Display for TokenTree { 61 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 62 | // pretty formatting for tokens 63 | // just use debug if you're not printing a message or something. 64 | use TokenTree::*; 65 | let message = match self { 66 | Block(_) => "tokens grouped by curly brackets".to_string(), 67 | List(_) => "tokens grouped by square brackets".to_string(), 68 | Form(_) => "a group of tokens".to_string(), 69 | Iden(i) => format!("identifier `{}`", i), 70 | Label(i) => format!("type identifier `{}`", i), 71 | Op(o) => format!("operator `{}`", o), 72 | Lit(l) => format!("literal `{}`", l), 73 | }; 74 | 75 | write!(f, "{}", message) 76 | } 77 | } 78 | 79 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 80 | pub enum ResIden { 81 | Macro, 82 | Type, 83 | Effect, 84 | If, 85 | Match, 86 | Mod, 87 | } 88 | 89 | impl ResIden { 90 | pub fn try_new(name: &str) -> Option { 91 | use ResIden::*; 92 | Some(match name { 93 | "macro" => Macro, 94 | "type" => Type, 95 | "effect" => Effect, 96 | "if" => If, 97 | "match" => Match, 98 | "mod" => Mod, 99 | _ => { 100 | return None; 101 | } 102 | }) 103 | } 104 | } 105 | 106 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 107 | pub enum ResOp { 108 | Assign, 109 | Lambda, 110 | Equal, 111 | Pow, 112 | Compose, 113 | Field, 114 | Is, 115 | Pair, 116 | Add, 117 | Sub, 118 | Mul, 119 | Div, 120 | Rem, 121 | } 122 | 123 | impl ResOp { 124 | pub fn try_new(name: &str) -> Option { 125 | use ResOp::*; 126 | Some(match name { 127 | "=" => Assign, 128 | "->" => Lambda, 129 | "==" => Equal, 130 | "**" => Pow, 131 | "|>" => Compose, 132 | "." => Field, 133 | ":" => Is, 134 | "," => Pair, 135 | "+" => Add, 136 | "-" => Sub, 137 | "*" => Mul, 138 | "/" => Div, 139 | "%" => Rem, 140 | _ => { 141 | return None; 142 | } 143 | }) 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /passerine-common/src/data.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | collections::BTreeMap, 3 | f64, 4 | fmt::{Debug, Display, Formatter, Result}, 5 | rc::Rc, 6 | }; 7 | 8 | use crate::{closure::Closure, lambda::Lambda}; 9 | 10 | // TODO: separate VM data from parser data 11 | 12 | /// Built-in Passerine datatypes. 13 | #[derive(Clone, PartialEq)] 14 | pub enum Data { 15 | // Passerine Data (Atomic) 16 | /// Float Numbers, represented as double-precision floating points. 17 | Float(f64), 18 | // TODO: arbitrary precision integers. 19 | /// Integers, currently 64-bit. 20 | Integer(i64), 21 | 22 | /// A boolean, like true or false. 23 | Boolean(bool), 24 | /// A UTF-8 encoded string. 25 | String(String), 26 | 27 | /// Represents a function, ie.e some bytecode without a context. 28 | Lambda(Rc), 29 | 30 | /// Some bytecode with a context that can be run. 31 | Closure(Box), 32 | 33 | // TODO: just remove Kind 34 | /// `Kind` is the base component of an unconstructed label 35 | Kind(usize), 36 | /// A Label is similar to a type, and wraps some data. 37 | /// in the future labels will have associated namespaces. 38 | Label(usize, Box), 39 | 40 | // TODO: equivalence between Unit and Tuple(vec![])? 41 | 42 | // Compound Datatypes 43 | /// The empty Tuple 44 | Unit, // an empty typle 45 | /// A non-empty Tuple. 46 | Tuple(Vec), 47 | // // TODO: Hashmap? 48 | // // I mean, it's overkill for small things 49 | // // yet if people have very big records, yk. 50 | Record(BTreeMap), 51 | Map(BTreeMap), 52 | // ArbInt(ArbInt), 53 | } 54 | 55 | // TODO: manually implement the equality trait 56 | // NOTE: might have to implement partial equality as well 57 | // NOTE: equality represents passerine equality, not rust equality 58 | impl Eq for Data {} 59 | 60 | impl Display for Data { 61 | /// Displays some Passerine Data in a pretty manner, as if it were printed 62 | /// to console. 63 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 64 | match self { 65 | // Data::Heaped(_) => unreachable!("Can not display heaped data"), 66 | // Data::NotInit => unreachable!("Can not display uninitialized 67 | // data"), 68 | Data::Float(n) => write!(f, "{}", n), 69 | Data::Integer(n) => write!(f, "{}", n), 70 | Data::Boolean(b) => { 71 | write!(f, "{}", if *b { "true" } else { "false" }) 72 | } 73 | Data::String(s) => write!(f, "{}", s), 74 | Data::Lambda(_) => unreachable!("Can not display naked functions"), 75 | Data::Closure(_) => write!(f, "Function"), 76 | Data::Kind(_) => unreachable!("Can not display naked labels"), 77 | Data::Label(n, v) => write!(f, "{} {}", n, v), 78 | Data::Unit => write!(f, "()"), 79 | Data::Tuple(t) => write!( 80 | f, 81 | "({})", 82 | t.iter() 83 | .map(|i| format!("{}", i)) 84 | .collect::>() 85 | .join(", ") 86 | ), 87 | // TODO: create representation 88 | _ => panic!("Representation is not yet implemented for this"), 89 | } 90 | } 91 | } 92 | 93 | impl Debug for Data { 94 | /// Displays some Passerine Data following Rust conventions, 95 | /// with certain fields omitted. 96 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 97 | match self { 98 | // Data::Heaped(h) => write!(f, "Heaped({:?})", h), 99 | // Data::NotInit => write!(f, "NotInit"), 100 | Data::Float(n) => write!(f, "Float({:?})", n), 101 | Data::Integer(n) => write!(f, "Integer({:?})", n), 102 | Data::Boolean(b) => write!(f, "Boolean({:?})", b), 103 | Data::String(s) => write!(f, "String({:?})", s), 104 | Data::Lambda(_) => write!(f, "Function(...)"), 105 | Data::Closure(_c) => write!(f, "Closure(...)"), /* TODO: how to */ 106 | // differentiate? 107 | Data::Kind(n) => write!(f, "Kind({})", n), 108 | Data::Label(n, v) => write!(f, "Label({}, {:?})", n, v), 109 | Data::Unit => write!(f, "Unit"), 110 | Data::Tuple(t) => write!(f, "Tuple({:?})", t), 111 | // TODO: create representation 112 | _ => panic!("Representation is not yet implemented for this"), 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /passerine/src/compiler/syntax.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::common::span::Span; 4 | 5 | /// Represents a note attached to a Syntax error, 6 | /// i.e. a location in source code with an optional 7 | /// specific hint or tip corresponding this this specific location 8 | #[derive(Debug, PartialEq, Eq)] 9 | pub struct Note { 10 | pub span: Span, 11 | pub hint: Option, 12 | } 13 | 14 | impl Note { 15 | pub fn new(span: Span) -> Note { 16 | Note { span, hint: None } 17 | } 18 | 19 | pub fn new_with_hint(hint: &str, span: &Span) -> Note { 20 | Note { 21 | span: span.clone(), 22 | hint: Some(hint.to_string()), 23 | } 24 | } 25 | } 26 | 27 | /// Represents a static error (syntax, semantics, etc.) found at compile time. 28 | /// Ideally, each note included should have a distinct `Span` and hint. 29 | /// Usually, one `Note` per error is enough. 30 | #[derive(Debug, PartialEq, Eq)] 31 | pub struct Syntax { 32 | pub reason: String, 33 | pub notes: Vec, 34 | } 35 | 36 | impl Syntax { 37 | /// Creates a new static error with a single note that does not have a hint. 38 | pub fn error(reason: &str, span: &Span) -> Syntax { 39 | Syntax::error_with_note( 40 | reason, 41 | Note { 42 | span: span.clone(), 43 | hint: None, 44 | }, 45 | ) 46 | } 47 | 48 | /// Creates a new static error with a single note that may or may not have a 49 | /// hint. 50 | pub fn error_with_note(reason: &str, note: Note) -> Syntax { 51 | Syntax { 52 | reason: reason.to_string(), 53 | notes: vec![note], 54 | } 55 | } 56 | 57 | /// Creates a syntax error without a note. This syntax error will not 58 | /// contain any location information, so only use it if you plan to add 59 | /// additional notes with [`add_note`] later. 60 | pub fn error_no_note(reason: &str) -> Syntax { 61 | Syntax { 62 | reason: reason.to_string(), 63 | notes: vec![], 64 | } 65 | } 66 | 67 | /// Extend a syntax error by adding another note to the error. 68 | pub fn add_note(mut self, note: Note) -> Self { 69 | self.notes.push(note); 70 | self 71 | } 72 | } 73 | 74 | impl fmt::Display for Syntax { 75 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 76 | for note in self.notes.iter() { 77 | let formatted = note.span.format(); 78 | 79 | if let Some(ref hint) = note.hint { 80 | if formatted.is_multiline() { 81 | writeln!(f, "{}", formatted)?; 82 | writeln!(f, "{} |- note: {} ", formatted.gutter_padding(), hint)?; 83 | writeln!(f, "{} |", " ".repeat(formatted.gutter_padding()))?; 84 | } else { 85 | writeln!( 86 | f, 87 | "In {}:{}:{}", 88 | formatted.path, formatted.start, formatted.start_col 89 | )?; 90 | writeln!(f, "{} |", " ".repeat(formatted.gutter_padding()))?; 91 | writeln!(f, "{} | {}", formatted.start + 1, formatted.lines[0])?; 92 | writeln!( 93 | f, 94 | "{} | {}{} note: {}", 95 | " ".repeat(formatted.gutter_padding()), 96 | " ".repeat(formatted.start_col), 97 | "^".repeat(formatted.carrots().unwrap()), 98 | hint, 99 | )?; 100 | writeln!(f, "{} |", " ".repeat(formatted.gutter_padding()))?; 101 | } 102 | } else { 103 | write!(f, "{}", formatted)?; 104 | } 105 | } 106 | write!(f, "Syntax Error: {}", self.reason) 107 | } 108 | } 109 | 110 | #[cfg(test)] 111 | mod test { 112 | use std::rc::Rc; 113 | 114 | use super::*; 115 | use crate::common::source::Source; 116 | 117 | #[test] 118 | fn error() { 119 | // This is just a demo to check formatting 120 | // might not coincide with an actual Passerine error 121 | let source = Rc::new(Source::source("x = \"Hello, world\" -> y + 1")); 122 | let error = Syntax::error( 123 | "Unexpected token '\"Hello, world!\"'", 124 | &Span::new(&source, 4, 14), 125 | ); 126 | 127 | let target = r#"In ./source:1:5 128 | | 129 | 1 | x = "Hello, world" -> y + 1 130 | | ^^^^^^^^^^^^^^ 131 | Syntax Error: Unexpected token '"Hello, world!"'"#; 132 | 133 | let result = format!("{}", error); 134 | assert_eq!(result, target); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /passerine-derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | use proc_macro::TokenStream; 2 | use quote::{quote, quote_spanned}; 3 | use syn::{parse_macro_input, spanned::Spanned, DeriveInput, Ident, Index}; 4 | 5 | /// A derive macro that generates an implementation of the `Inject` trait, 6 | /// which allows a Rust type to be converted to Passerine data and back again. 7 | /// This type is very important for building interfaces between Rust and 8 | /// Passerine using system injection. 9 | #[proc_macro_derive(Effect)] 10 | pub fn derive_effect(input: TokenStream) -> TokenStream { 11 | // Parse the input tokens into a syntax tree 12 | let input = parse_macro_input!(input as DeriveInput); 13 | let type_name = input.ident; 14 | 15 | let (from, into) = match input.data { 16 | syn::Data::Struct(ref data) => match data.fields { 17 | syn::Fields::Named(ref fields) => derive_struct_named(&type_name, fields), 18 | syn::Fields::Unnamed(ref fields) => derive_struct_unnamed(&type_name, fields), 19 | syn::Fields::Unit => { 20 | let from = quote! { 21 | if let passerine_common::Data::Unit = param { 22 | Some(#type_name) 23 | } else { 24 | None 25 | } 26 | }; 27 | let into = quote! { passerine_common::Data::Unit }; 28 | (from, into) 29 | } 30 | }, 31 | syn::Data::Enum(ref _data) => todo!(), 32 | syn::Data::Union(ref _data) => { 33 | unimplemented!("Unions are not supported") 34 | } 35 | }; 36 | 37 | // Build the output, possibly using quasi-quotation 38 | let expanded = quote! { 39 | impl passerine_common::Inject for #type_name { 40 | fn serialize(param: Self) -> passerine_common::Data { #into } 41 | fn deserialize(param: passerine_common::Data) -> Option { #from } 42 | } 43 | 44 | // // Data -> Item conversion 45 | // impl TryFrom for #type_name { 46 | // type Error = (); 47 | // fn try_from(param: passerine_common::Data) -> Result { #from } 48 | // } 49 | 50 | // // Item -> Data conversion 51 | // impl From<#type_name> for passerine_common::Data { 52 | // fn from(param: #type_name) -> Self { #into } 53 | // } 54 | 55 | // // With the above two implemented, 56 | // // we can implement inject automatically. 57 | // impl passerine_common::Inject for #type_name {} 58 | }; 59 | 60 | // Hand the output tokens back to the compiler 61 | TokenStream::from(expanded) 62 | } 63 | 64 | fn derive_struct_named( 65 | type_name: &Ident, 66 | fields: &syn::FieldsNamed, 67 | ) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { 68 | let num_fields = fields.named.len(); 69 | let from = fields.named.iter().rev().map(|f| { 70 | let name = &f.ident; 71 | quote_spanned! { f.span() => 72 | #name: passerine_common::Inject::deserialize(param.pop()?)? 73 | } 74 | }); 75 | let into = fields.named.iter().map(|f| { 76 | let name = &f.ident; 77 | quote_spanned! { f.span() => 78 | passerine_common::Inject::serialize(param.#name) 79 | } 80 | }); 81 | 82 | let from = quote! { 83 | if let passerine_common::Data::Tuple(mut param) = param { 84 | if param.len() != #num_fields { return None; } 85 | Some(#type_name { #(#from,)* }) 86 | } else { 87 | None 88 | } 89 | }; 90 | let into = quote! { 91 | passerine_common::Data::Tuple(vec![#(#into,)*]) 92 | }; 93 | 94 | (from, into) 95 | } 96 | 97 | fn derive_struct_unnamed( 98 | type_name: &Ident, 99 | fields: &syn::FieldsUnnamed, 100 | ) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { 101 | let num_fields = fields.unnamed.len(); 102 | let from = fields.unnamed.iter().rev().map(|f| { 103 | quote_spanned! { f.span() => 104 | passerine_common::Inject::deserialize(param.pop()?)? 105 | } 106 | }); 107 | let into = fields.unnamed.iter().enumerate().map(|(index, f)| { 108 | let index = Index::from(index); 109 | quote_spanned! { f.span() => 110 | passerine_common::Inject::serialize(param.#index) 111 | } 112 | }); 113 | 114 | let from = quote! { 115 | if let passerine_common::Data::Tuple(mut param) = param { 116 | if param.len() != #num_fields { return None; } 117 | Some(#type_name (#(#from,)*)) 118 | } else { 119 | None 120 | } 121 | }; 122 | let into = quote! { 123 | passerine_common::Data::Tuple(vec![#(#into,)*]) 124 | }; 125 | 126 | (from, into) 127 | } 128 | -------------------------------------------------------------------------------- /passerine/src/construct/tree.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use crate::{ 4 | common::{lit::Lit, span::Spanned}, 5 | construct::{ 6 | scope::Scope, 7 | symbol::{SharedSymbol, UniqueSymbol}, 8 | token::ResIden, 9 | }, 10 | }; 11 | 12 | #[derive(Debug, Clone, PartialEq)] 13 | pub enum Pattern { 14 | Symbol(S), 15 | Lit(Lit), 16 | Label(Spanned, Box>), 17 | Tuple(Vec>), 18 | Chain(Vec>), 19 | } 20 | 21 | impl Pattern { 22 | pub fn label(symbol: Spanned, pattern: Spanned) -> Self { 23 | Pattern::Label(symbol, Box::new(pattern)) 24 | } 25 | } 26 | 27 | // TODO: impls for boxed items. 28 | 29 | #[derive(Debug, Clone, PartialEq)] 30 | pub enum Base { 31 | Symbol(S), 32 | Label(S), 33 | Lit(Lit), 34 | Tuple(Vec), 35 | Module(Box), 36 | 37 | Block(Vec), 38 | Call(Box, Box), // fun, arg 39 | Assign(Spanned>, Box), 40 | Effect(S), 41 | } 42 | 43 | impl Base { 44 | pub fn call(fun: T, arg: T) -> Self { 45 | Base::Call(Box::new(fun), Box::new(arg)) 46 | } 47 | 48 | pub fn assign(pat: Spanned>, expr: T) -> Self { 49 | Base::Assign(pat, Box::new(expr)) 50 | } 51 | 52 | pub fn module(module: T) -> Self { 53 | Base::Module(Box::new(module)) 54 | } 55 | 56 | // pub fn ffi(name: &str, expr: T) -> Self { 57 | // Base::FFI(name.to_string(), Box::new(expr)) 58 | // } 59 | } 60 | 61 | #[derive(Debug, Clone, PartialEq)] 62 | pub enum Sugar { 63 | Group(Box), 64 | Form(Vec), 65 | Keyword(ResIden), 66 | // Pattern(Pattern), 67 | // Record, 68 | Is(Box, Box), // expr, type 69 | // A function composition 70 | Comp(Box, Box), // arg, function 71 | Field(Box, Box), /* struct, field 72 | * TODO: math operators */ 73 | } 74 | 75 | impl Sugar { 76 | pub fn group(tree: T) -> Self { 77 | Sugar::Group(Box::new(tree)) 78 | } 79 | 80 | pub fn is(expr: T, ty: T) -> Self { 81 | Sugar::Is(Box::new(expr), Box::new(ty)) 82 | } 83 | 84 | pub fn comp(arg: T, fun: T) -> Self { 85 | Sugar::Comp(Box::new(arg), Box::new(fun)) 86 | } 87 | 88 | pub fn field(record: T, name: T) -> Self { 89 | Sugar::Field(Box::new(record), Box::new(name)) 90 | } 91 | } 92 | 93 | #[derive(Debug, Clone, PartialEq)] 94 | pub struct Lambda { 95 | pub arg: Spanned>, 96 | pub body: Box, 97 | } 98 | 99 | impl Lambda { 100 | pub fn new(arg: Spanned>, body: T) -> Self { 101 | Lambda { 102 | arg, 103 | body: Box::new(body), 104 | } 105 | } 106 | } 107 | 108 | #[derive(Debug, Clone, PartialEq)] 109 | pub enum AST { 110 | Base(Base, SharedSymbol>), 111 | Sugar(Sugar>), 112 | Lambda(Lambda>), 113 | } 114 | 115 | impl TryFrom for Pattern { 116 | type Error = String; 117 | 118 | /// Tries to convert an `AST` into a `Pattern`. 119 | /// Patterns mirror the `AST`s they are designed to 120 | /// destructure. During parsing, they are just 121 | /// parsed as `AST`s - When the compiler can 122 | /// determine that an AST is actually a pattern, 123 | /// It performs this conversion. 124 | fn try_from(ast: AST) -> Result { 125 | // if true { todo!("SharedSymbol lookup"); } 126 | Ok(match ast { 127 | AST::Base(Base::Symbol(s)) => Pattern::Symbol(s), 128 | AST::Base(Base::Lit(d)) => Pattern::Lit(d), 129 | AST::Base(Base::Label(k)) => Err(format!( 130 | "This Label used in a pattern does not unwrap any data.\n\ 131 | To match a Label and ignore its contents, use `{:?} _`", 132 | k, 133 | ))?, 134 | AST::Base(Base::Tuple(t)) => { 135 | let mut patterns = vec![]; 136 | for item in t { 137 | patterns.push(item.try_map(Pattern::try_from)?); 138 | } 139 | Pattern::Tuple(patterns) 140 | } 141 | 142 | // AST::Sugar(Sugar::Pattern(p)) => p, 143 | AST::Sugar(Sugar::Form(f)) => { 144 | let mut patterns = vec![]; 145 | for item in f { 146 | patterns.push(item.try_map(Pattern::try_from)?); 147 | } 148 | Pattern::Chain(patterns) 149 | } 150 | AST::Sugar(Sugar::Group(e)) => e.try_map(Pattern::try_from)?.item, 151 | _ => Err("Unexpected construct inside pattern")?, 152 | }) 153 | } 154 | } 155 | 156 | #[derive(Debug, Clone, PartialEq)] 157 | pub enum CST { 158 | Base(Base, SharedSymbol>), 159 | Lambda(Lambda>), 160 | } 161 | 162 | #[derive(Debug, Clone, PartialEq)] 163 | pub struct ScopedLambda { 164 | pub arg: Spanned>, 165 | pub body: Box, 166 | pub scope: Scope, 167 | } 168 | 169 | impl ScopedLambda { 170 | pub fn new(arg: Spanned>, tree: T, scope: Scope) -> Self { 171 | ScopedLambda { 172 | arg, 173 | body: Box::new(tree), 174 | scope, 175 | } 176 | } 177 | } 178 | 179 | #[derive(Debug, Clone, PartialEq)] 180 | pub enum SST { 181 | Base(Base, UniqueSymbol>), 182 | ScopedLambda(ScopedLambda>), 183 | } 184 | -------------------------------------------------------------------------------- /passerine/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::needless_return)] 2 | // TODO: Re-write introductory documentation 3 | 4 | //! # Passerine 5 | //! This repository contains the core of the Passerine Programming Language, 6 | //! including the compiler, VM, and various utilities. 7 | //! 8 | //! ## Running Passerine 9 | //! Passerine is primarily run through Aspen, 10 | //! Passerine's package manager. 11 | //! Simply install Aspen, then: 12 | //! ```bash 13 | //! $ aspen new first-package 14 | //! $ cd first-package 15 | //! $ aspen run 16 | //! ``` 17 | //! Aspen is the idiomatic way to create and run Passerine packages. 18 | //! The documentation that follows is about the core compiler itself. 19 | //! This is only useful if you're trying to embed Passerine in your Rust project 20 | //! or developing the core Passerine compiler and VM. 21 | //! If that's the case, read on! 22 | //! 23 | //! You can install Passerine by following the instructions at 24 | //! [passerine.io](https://www.passerine.io/#install). 25 | //! 26 | //! ## Embedding Passerine in Rust 27 | //! > TODO: Clean up crate visibility, create `run` function. 28 | //! 29 | //! Add passerine to your `Cargo.toml`: 30 | //! ```toml 31 | //! # make sure this is the latest version 32 | //! passerine = 0.9 33 | //! ``` 34 | //! Then simply: 35 | //! ```ignore 36 | //! // DISCLAIMER: The `run` function used here has not been implemented yet, 37 | //! // although the underlying interface is mostly stable. 38 | //! 39 | //! use passerine; 40 | //! 41 | //! fn main() { 42 | //! passerine::run("print \"Hello from Passerine!\""); 43 | //! } 44 | //! ``` 45 | //! 46 | //! ## Overview of the compilation process 47 | //! > NOTE: For a more detail, read through the documentation 48 | //! for any of the components mentioned. 49 | //! 50 | //! Within the compiler pipeline, source code is represented as a `Source` 51 | //! object. A source is a reference to some code, with an associated path 52 | //! telling which file it came from. 53 | //! 54 | //! Regions of source code can be marked with `Span`s, 55 | //! Which are like `&strs` but with a reference-counted reference to the 56 | //! original `Source`, methods for combining them, and so on. 57 | //! Spans are used throughout the compiler when reporting errors. 58 | //! Compiler Datastructures can be `Spanned` to indicate where they originated. 59 | //! 60 | //! ### Compilation 61 | //! Compilation steps can raise `Err(Syntax)`, 62 | //! indicating that an error occured. 63 | //! `Syntax` is just a `Span` and a message, 64 | //! which can be pretty-printed. 65 | //! 66 | //! The first phase of compilation is lexing. 67 | //! The `Lexer` reads through a source, and produces a stream of 68 | //! `Spanned`s. The `Lexer` is super simple - it greedily looks for the 69 | //! longest next token, Then consumes it and advances by the token's length. 70 | //! To lex a file, use the `compiler::lex::lex` function. 71 | //! 72 | //! The next phase of compilation is parsing. 73 | //! The parser takes a spanned token stream, 74 | //! and builts a spanned Abstract Syntax Tree (AST). 75 | //! The parser used is a modified Pratt parser. 76 | //! (It's modified to handle the special function-call syntax used.) 77 | //! To parse a token stream, use the `compiler::parse::parse` function. 78 | //! 79 | //! The AST is then traversed and simplified; 80 | //! this is where macro expansion and so on take place. 81 | //! The result is a simplified Concrete Syntax Tree (CST). 82 | //! 83 | //! After constructing the CST, bytecode is generated. 84 | //! Bytecode is just a vector of u8s, interlaced with split numbers. 85 | //! All the opcodes are defined in `common::opcode`, 86 | //! And implemented in `compiler::vm::vm`. 87 | //! A bytecode object is a called a `Lambda`. 88 | //! The bytecode generator works by walking the CST, 89 | //! Recursively nesting itself when a new scope is encountered. 90 | //! To generate bytecode for an CST, use the `compiler::gen::gen` function. 91 | //! 92 | //! ### Execution 93 | //! The VM can raise `Err(Trace)` if it encounters 94 | //! errors during execution. 95 | //! A `Trace` is similar to `Syntax`, but it keeps track of 96 | //! multiple spans representing function calls and so on. 97 | //! 98 | //! After this, raw `Lambda`s are passed to the `VM` to be run. 99 | //! before being run by the `VM`, `Lambdas` are wrapped in `Closure`s, 100 | //! which hold some extra context. 101 | //! 102 | //! The `VM` is just a simple light stack-based VM. 103 | 104 | pub use passerine_common as common; 105 | pub mod compiler; 106 | pub mod construct; 107 | pub mod kernel; 108 | pub mod vm; 109 | 110 | // exported functions: 111 | // TODO: clean up exports 112 | 113 | use std::rc::Rc; 114 | 115 | pub use common::{closure::Closure, Data, Inject, Source}; 116 | pub use compiler::{ 117 | compile_source, 118 | syntax::Syntax, 119 | // TODO: 120 | Compiler, 121 | Desugarer, 122 | Hoister, 123 | Lexer, 124 | Parser, 125 | Reader, 126 | }; 127 | pub use passerine_derive::Effect; 128 | pub use vm::{fiber::Fiber, trace::Trace}; 129 | 130 | /// Compiles a [`Source`] to some bytecode. 131 | pub fn compile(source: Rc) -> Result { 132 | let bytecode = compile_source(source)?; 133 | return Ok(Closure::wrap(bytecode)); 134 | } 135 | 136 | // /// Compiles a [`Source`] to some bytecode, 137 | // /// With a specific [`FFI`]. 138 | // pub fn compile_with_ffi( 139 | // source: Rc, 140 | // ffi: FFI, 141 | // ) -> Result { 142 | // let tokens = ThinModule::thin(source).lower()?; 143 | // let ast = tokens.lower()?; 144 | // let cst = ast.lower()?; 145 | // let sst = cst.lower()?; 146 | // let sst_ffi = Module::new(sst.repr, (sst.assoc, ffi)); 147 | // let bytecode = sst_ffi.lower()?; 148 | 149 | // return Ok(Closure::wrap(bytecode)); 150 | // } 151 | 152 | // /// Run a compiled [`Closure`]. 153 | // pub fn run(closure: Closure) -> Result<(), Trace> { 154 | // let mut vm = VM::init(closure); 155 | // vm.run()?; 156 | // Ok(()) 157 | // } 158 | -------------------------------------------------------------------------------- /passerine-qualm/src/code.rs: -------------------------------------------------------------------------------- 1 | use crate::Pointer; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | #[repr(u8)] 5 | pub enum OpCode { 6 | // Unsigned 64-bit natural numbers 7 | NatAdd, 8 | NatSub, 9 | NatMul, 10 | NatDiv, 11 | NatShl, 12 | NatShr, 13 | NatBitAnd, 14 | NatBitOr, 15 | NatBitXor, 16 | NatEq, 17 | NatToFloat, 18 | 19 | // Signed 64-bit integers 20 | IntAdd, 21 | IntSub, 22 | IntMul, 23 | IntDiv, 24 | IntEq, 25 | IntToFloat, 26 | 27 | // Signed 64-bit floating point numbers 28 | // NaNs are not allowed 29 | FloatAdd, 30 | FloatSub, 31 | FloatMul, 32 | FloatDiv, 33 | FloatPartialEq, 34 | 35 | // Boolean 36 | BoolAnd, 37 | BoolOr, 38 | BoolXor, 39 | BoolNot, 40 | BoolEq, 41 | 42 | // Vaporization 43 | AllocSingle, // R1 _ -> owned pointer 44 | AllocPair, // R1 R2 -> owned pointer 45 | AllocMany, // SIZE _ -> owned pointer 46 | Borrow, // PTR _ -> borrowed pointer 47 | ReadSingle, // PTR IDX -> R3 48 | ReadMany, // PTR IDX SIZE -> onto stack 49 | WriteOwned, // owned_pointer idx val 50 | Write, // PTR SIZE -> R3 owned pointer / (loc of copying code on stack) 51 | 52 | // Pointer 53 | PointerIsOwned, // PTR _ -> bool 54 | PointerIsBorrowed, // PTR _ -> bool 55 | 56 | // Between Stack and register 57 | PushSingle, // R1 _ -> onto stack 58 | PushPair, // R1 R2 -> onto stack 59 | PushTriple, // R1 R2 R3 -> onto stack 60 | PopSingle, // R1 61 | PopPair, // R1 R2 62 | PopTriple, // R1 R2 R3 63 | 64 | // Stack 65 | DelSingle, // R1 66 | DelPair, // R1 R2 67 | DelTriple, // R1 R2 R3 68 | StackConst, // Idx 69 | 70 | // Register 71 | RegConst, // Idx _ -> R3 72 | RegSwap, // R1 _ -> R3 73 | 74 | // Control Flow 75 | JumpTrue, // loc R1 -> _ 76 | JumpFalse, // loc R1 -> _ 77 | JumpBranch, // loc1 R1 loc2 78 | Call, // code num_args num_captures 79 | Return, // R1 _ -> _ 80 | ReturnMany, // R1 num_ret -> _ 81 | 82 | // Higher Order 83 | ClosureSingle, // loc R2 -> owned pointer to closure 84 | Closure, // loc num_captures -> owned pointer to closure 85 | 86 | // Fibers and Handlers 87 | Fiber, // code num_captures -> owned pointer to fiber 88 | FiberCall, // arg fiber -> R3 89 | FiberYield, // arg _ -> R3 90 | HandlerAdd, // effect fiber -> _ 91 | HandlerCall, // effect -> R3 92 | } 93 | 94 | #[derive(Debug, Clone)] 95 | pub struct Instr { 96 | op_code: OpCode, 97 | r0: u8, 98 | r1: u8, 99 | out: u8, 100 | } 101 | 102 | impl Instr { 103 | pub fn exec(&self, regs: &mut [u64]) -> () { 104 | let r0 = self.r0 as usize; 105 | let r1 = self.r1 as usize; 106 | let out = self.out as usize; 107 | 108 | use std::mem::transmute as tm; 109 | use OpCode::*; 110 | 111 | // SAFETY: arguments gauranteed to be bitwise representation of ints: size(i64) == size(u64) 112 | let int_binop = |f: fn(i64, i64) -> i64, a0: u64, a1: u64| unsafe { 113 | let b0: i64 = tm(a0); 114 | let b1: i64 = tm(a1); 115 | tm(f(b0, b1)) 116 | }; 117 | 118 | // SAFETY: arguments gauranteed to be bitwise representation of floats: size(f64) == size(u64) 119 | let float_binop = |f: fn(f64, f64) -> f64, a0: u64, a1: u64| unsafe { 120 | let b0: f64 = tm(a0); 121 | let b1: f64 = tm(a1); 122 | tm(f(b0, b1)) 123 | }; 124 | 125 | match self.op_code { 126 | NatAdd => { regs[out] = regs[r0] + regs[r1] }, 127 | NatSub => { regs[out] = regs[r0] - regs[r1] }, 128 | NatMul => { regs[out] = regs[r0] * regs[r1] }, 129 | NatDiv => { regs[out] = regs[r0] / regs[r1] }, 130 | NatShl => { regs[out] = regs[r0] << regs[r1] }, 131 | NatShr => { regs[out] = regs[r0] >> regs[r1] }, 132 | NatBitAnd => { regs[out] = regs[r0] & regs[r1] }, 133 | NatBitOr => { regs[out] = regs[r0] | regs[r1] }, 134 | NatBitXor => { regs[out] = regs[r0] ^ regs[r1] }, 135 | NatEq => { todo!("booleans!"); }, 136 | 137 | // Signed 64-bit integers 138 | IntAdd => { regs[out] = int_binop(|a, b| { a + b }, regs[r0], regs[r1]) }, 139 | IntSub => { regs[out] = int_binop(|a, b| { a - b }, regs[r0], regs[r1]) }, 140 | IntMul => { regs[out] = int_binop(|a, b| { a * b }, regs[r0], regs[r1]) }, 141 | IntDiv => { regs[out] = int_binop(|a, b| { a / b }, regs[r0], regs[r1]) }, 142 | IntEq => { todo!("booleans!") }, 143 | 144 | // Signed 64-bit floating point numbers 145 | // NaNs are not allowed 146 | FloatAdd => { regs[out] = float_binop(|a, b| { a + b }, regs[r0], regs[r1]) }, 147 | FloatSub => { regs[out] = float_binop(|a, b| { a - b }, regs[r0], regs[r1]) }, 148 | FloatMul => { regs[out] = float_binop(|a, b| { a * b }, regs[r0], regs[r1]) }, 149 | FloatDiv => { regs[out] = float_binop(|a, b| { a / b }, regs[r0], regs[r1]) }, 150 | FloatPartialEq => { todo!("booleans!") }, 151 | 152 | // Boolean 153 | | BoolAnd 154 | | BoolOr 155 | | BoolXor 156 | | BoolNot 157 | | BoolEq => { todo!("booleans!") }, 158 | 159 | _ => todo!("not yep implemented"), 160 | 161 | // // Vaporization 162 | // AllocSingle, // R1 _ -> owned pointer 163 | // AllocPair, // R1 R2 -> owned pointer 164 | // AllocMany, // SIZE _ -> owned pointer 165 | // Borrow, // PTR _ -> borrowed pointer 166 | // ReadSingle, // PTR IDX -> R3 167 | // ReadMany, // PTR IDX SIZE -> onto stack 168 | // WriteSingle, // PTR IDX VAL -> copy on write 169 | // WriteMany, // PTR IDX SIZE -> from stack 170 | // 171 | // // Pointer 172 | PointerIsOwned => { }, 173 | // PointerIsBorrowed, // PTR _ -> bool 174 | // 175 | // // Between Stack and register 176 | // PushSingle, // R1 _ -> onto stack 177 | // PushPair, // R1 R2 -> onto stack 178 | // PushTriple, // R1 R2 R3 -> onto stack 179 | // PopSingle, // R1 180 | // PopPair, // R1 R2 181 | // PopTriple, // R1 R2 R3 182 | // 183 | // // Stack 184 | // DelSingle, // R1 185 | // DelPair, // R1 R2 186 | // DelTriple, // R1 R2 R3 187 | // StackConst, // Idx 188 | // 189 | // // Register 190 | // RegConst, // Idx _ -> R3 191 | // RegSwap, // R1 _ -> R3 192 | // 193 | // // Control Flow 194 | // JumpTrue, // loc R1 -> _ 195 | // JumpFalse, // loc R1 -> _ 196 | // Call, // code num_args num_captures 197 | // Return, // R1 _ -> _ 198 | // ReturnMany, // R1 num_ret -> _ 199 | // 200 | // // Higher Order 201 | // ClosureSingle, // loc R2 -> owned pointer to closure 202 | // Closure, // loc num_captures -> owned pointer to closure 203 | // 204 | // // Fibers and Handlers 205 | // Fiber, // code num_captures -> owned pointer to fiber 206 | // FiberCall, // arg fiber -> R3 207 | // FiberYield, // arg _ -> R3 208 | // HandlerAdd, // effect fiber -> _ 209 | // HandlerCall, // effect -> R3 210 | } 211 | todo!() 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /passerine-qualm/src/heap/range_set.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet}; 2 | 3 | use crate::heap::pointer::{Pointer, PointerIdx}; 4 | 5 | // Needs to do a few simple things: 6 | // Returns all ranges greater than or equal to a given size 7 | // when a range is added, merges neighboring ranges together 8 | // when a range is removed, splits neighboring ranges 9 | 10 | /// Keeps track of unallocated ranges of slots. 11 | /// When a pointer is freed, it's range is merged with other ranges. 12 | /// We use a pair of BTreeMaps to keep this snappy under the hood. 13 | #[derive(Debug)] 14 | pub(super) struct RangeSet { 15 | pub(super) capacity: usize, 16 | // slots before, length of range 17 | pub(super) ranges: BTreeMap, 18 | // length -> start of range 19 | // if an entry size is present in the map, the pointer set must be non-empty. 20 | pub(super) free: BTreeMap>, 21 | } 22 | 23 | impl RangeSet { 24 | /// Create a new RangeSet with no capacity 25 | pub fn new() -> RangeSet { 26 | RangeSet { 27 | capacity: 0, 28 | ranges: BTreeMap::new(), 29 | free: BTreeMap::new(), 30 | } 31 | } 32 | 33 | /// Adds some capacity to the heap. 34 | pub fn add_free_capacity(&mut self, slots: usize) { 35 | self.free(Pointer::new(PointerIdx::new(self.capacity as u64)), slots); 36 | self.capacity += slots; 37 | } 38 | 39 | /// Create a new rangeset with the capacity of a pre-allocated heap. 40 | pub fn new_with_free_capacity(slots: usize) -> RangeSet { 41 | let mut empty = RangeSet::new(); 42 | empty.add_free_capacity(slots); 43 | empty 44 | } 45 | 46 | /// Returns a pointer and the size to increase the allocation by. 47 | /// The backing allocation size must be increased according to the returned size. 48 | /// Do not call `add_free_capacity` with the returned size of this method, 49 | /// because the allocation is used, not free. 50 | pub fn mark_first(&mut self, slots: usize) -> (Pointer, usize) { 51 | // try filling the smallest earliest gap possible. 52 | if let Some((_size, potential)) = self.free.range(slots..).next() { 53 | let pointer = *potential.iter().next().unwrap(); 54 | self.mark_smaller(pointer, slots); 55 | return (Pointer::new(pointer), 0); 56 | } 57 | 58 | // if the last range is a tail range, try extending it 59 | if let Some((tail, size)) = self.ranges.iter().rev().next() { 60 | // copy to please the borrow checker gods 61 | let (tail, size) = (*tail, *size); 62 | // this free range goes right up to the end 63 | if tail.to_usize() + size == self.capacity { 64 | self.mark(tail); 65 | let remaining = slots - size; 66 | self.capacity += remaining; 67 | return (Pointer::new(tail), remaining) 68 | } 69 | } 70 | 71 | let pointer = Pointer::new(PointerIdx::new(self.capacity as u64)); 72 | self.capacity += slots; 73 | return (pointer, slots); 74 | } 75 | 76 | /// Mark a pointer for use reserving a certain number of slots, 77 | /// returns the extra free space to the heap. 78 | pub fn mark_smaller(&mut self, pointer: PointerIdx, slots: usize) { 79 | // grab the full allocation 80 | let size = self.mark(pointer); 81 | if size == slots { return; } 82 | 83 | // let go of the end; may cause minor fragmentation 84 | assert!(slots < size); 85 | self.free(Pointer::new(pointer.into()).add(slots as u64), size - slots); 86 | } 87 | 88 | /// Mark a pointer for use, returns the size of the full allocation 89 | fn mark(&mut self, pointer: PointerIdx) -> usize { 90 | // remove it from the ranges set, getting the size of the pointer 91 | let size = self.ranges.remove(&pointer).unwrap(); 92 | // remove it from the free set, by inverse looking up by size 93 | assert!(self.free.get_mut(&size).unwrap().remove(&pointer)); 94 | // if the pointer was the last of a given size, remove the entry from the map 95 | if self.free.get(&size).unwrap().is_empty() { 96 | self.free.remove_entry(&size); 97 | } 98 | // return the size of the marked pointer 99 | return size; 100 | } 101 | 102 | // TODO: tail allocations. 103 | /// Returns whether a pointer of a given size is free at a given point. 104 | /// Used to determine whether reallocation in place is possible. 105 | pub fn is_free(&self, pointer: Pointer, slots: usize) -> bool { 106 | let pointer: PointerIdx = pointer.to_idx(); 107 | 108 | // get the first pointer before or at the one specified. 109 | if let Some((p, free_range)) = self.ranges.range(..=pointer).rev().next() { 110 | // check that the free range covers the range of the pointer in question 111 | let p_end = p.to_usize() + free_range; 112 | let pointer_end = pointer.to_usize() + slots; 113 | 114 | // for a pointer to be free it must be in the range! 115 | if p_end >= pointer_end { 116 | return true; 117 | } 118 | 119 | // TODO: tail allocations; need to increase the allocation size. 120 | // || self.free.capacity == p_end 121 | // && self.free.capacity < pointer_end 122 | } 123 | 124 | false 125 | } 126 | 127 | /// Returns capacity that the heap can be shrunk by if freeing a tail allocation 128 | pub fn free(&mut self, pointer: Pointer, mut slots: usize) -> usize { 129 | let mut pointer: PointerIdx = pointer.into(); 130 | 131 | // merge it with any other nearby ranges 132 | // start with the range before 133 | if let Some((pointer_before, size)) = self.ranges.range(..pointer).rev().next() { 134 | let (pointer_before, size) = (*pointer_before, *size); 135 | 136 | // if the free ranges are back-to-back, we merge them by extending the old range 137 | if pointer_before + size as u64 == pointer { 138 | // use the new combined pointer 139 | self.mark(pointer_before); 140 | pointer = pointer_before; 141 | slots = size + slots; 142 | } 143 | } 144 | 145 | // then do the range after 146 | // pointer has not yet beed added to the ranges map, 147 | // so `pointer..` is technically exclusive 148 | if let Some((pointer_after, size)) = self.ranges.range(pointer..).next() { 149 | let (pointer_after, size) = (*pointer_after, *size); 150 | if pointer + size as u64 == pointer_after { 151 | // extend the pointer to be longer 152 | self.mark(pointer_after); 153 | slots += size; 154 | } 155 | } 156 | 157 | // if this is a tail free, reduce the size of the heap 158 | if pointer.to_usize() + slots== self.capacity { 159 | self.capacity -= slots; 160 | return slots; 161 | } 162 | 163 | // add the pointer with its new size in the free map 164 | // add the pointer with its new size to the ranges map 165 | if let Some(s) = self.free.get_mut(&slots) { 166 | s.insert(pointer); 167 | } else { 168 | let mut pointers = BTreeSet::new(); 169 | pointers.insert(pointer); 170 | self.free.insert(slots, pointers); 171 | } 172 | 173 | // not a tail free, there still may be an allocation after this one 174 | // return 0 to keep slots 175 | assert!(self.ranges.insert(pointer, slots).is_none()); 176 | return 0; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /passerine/src/vm/stack.rs: -------------------------------------------------------------------------------- 1 | use std::{cell::RefCell, mem, rc::Rc}; 2 | 3 | use crate::{ 4 | common::Data, 5 | vm::{ 6 | slot::{Slot, Suspend}, 7 | tag::Tagged, 8 | }, 9 | }; 10 | 11 | /// A stack of `Tagged` `Data`. 12 | /// Note that in general the stack is expected to follow the following pattern: 13 | /// ```plain 14 | /// FV...V...F V...T... ... 15 | /// ``` 16 | /// Or in other words, a frame followed by a block of *n* values that are locals 17 | /// followed by *n* temporaries, ad infinitum. 18 | #[derive(Debug)] 19 | pub struct Stack { 20 | pub frames: Vec, 21 | pub stack: Vec, 22 | } 23 | 24 | impl Stack { 25 | /// Create a new `Stack` with a single frame. 26 | pub fn init() -> Stack { 27 | Stack { 28 | frames: vec![0], 29 | stack: vec![Tagged::frame()], 30 | } 31 | } 32 | 33 | /// Return the index of the topmost `Tagged(Slot::Frame)`. 34 | #[inline] 35 | fn frame_index(&self) -> usize { 36 | *self.frames.last().unwrap() 37 | } 38 | 39 | /// Pop and return the topmost `Tagged` item. 40 | #[inline] 41 | fn pop(&mut self) -> Tagged { 42 | self.stack 43 | .pop() 44 | .expect("VM tried to pop empty stack, stack should never be empty") 45 | } 46 | 47 | /// Swaps out a `Tagged` item without another `Tagged` item, provided its 48 | /// index. 49 | #[inline] 50 | fn swap(&mut self, index: usize, tagged: Tagged) -> Tagged { 51 | mem::replace(&mut self.stack[index], tagged) 52 | } 53 | 54 | /// Pushes some `Data` onto the `Stack`, tagging it along the way 55 | #[inline] 56 | pub fn push_data(&mut self, data: Data) { 57 | self.stack.push(Tagged::new(Slot::Data(data))) 58 | } 59 | 60 | /// Pushes some `Tagged` `Data` onto the `Stack` without unwrapping it. 61 | #[inline] 62 | pub fn push_tagged(&mut self, tagged: Tagged) { 63 | self.stack.push(tagged) 64 | } 65 | 66 | /// Pops some `Data` of the `Stack`, panicking if what it pops is not 67 | /// `Data`. Note that this will never return a `Heaped` value, rather 68 | /// cloning the value inside. 69 | #[inline] 70 | pub fn pop_data(&mut self) -> Data { 71 | let value = self 72 | .stack 73 | .pop() 74 | .expect("VM tried to pop empty stack, stack should never be empty"); 75 | 76 | // match value.slot().data() { 77 | // // Data::Heaped(h) => h.borrow().clone(), 78 | // d => d, 79 | // } 80 | 81 | value.slot().data() 82 | } 83 | 84 | /// Pops a stack frame from the `Stack`, restoring the previous frame. 85 | /// Panics if there are no frames left on the stack. 86 | #[inline] 87 | pub fn pop_frame(&mut self) -> Suspend { 88 | if let Slot::Frame = self.pop().slot() { 89 | } else { 90 | unreachable!("Expected frame on top of stack"); 91 | } 92 | 93 | self.frames.pop(); 94 | let old_slot = self.swap(self.frame_index(), Tagged::frame()).slot(); 95 | 96 | if let Slot::Suspend(s) = old_slot { 97 | return s; 98 | } else { 99 | unreachable!("Expected frame on top of stack"); 100 | } 101 | } 102 | 103 | /// Pushes a new stack frame onto the `Stack`. 104 | /// Takes the old suspended closure / ip, and stores that on the stack. 105 | #[inline] 106 | pub fn push_frame(&mut self, suspend: Suspend) { 107 | let frame_index = self.frame_index(); 108 | self.stack[frame_index] = Tagged::new(Slot::Suspend(suspend)); 109 | self.frames.push(self.stack.len()); 110 | self.stack.push(Tagged::frame()); 111 | } 112 | 113 | /// Shorcut for pushing a `Tagged(Slot::NotInit)` on top of the stack. 114 | #[inline] 115 | pub fn push_not_init(&mut self) { 116 | self.stack.push(Tagged::not_init()); 117 | } 118 | 119 | /// Shortcut for calling `push_not_init` N times. 120 | #[inline] 121 | pub fn declare(&mut self, decls: usize) { 122 | for _ in 0..decls { 123 | self.push_not_init(); 124 | } 125 | } 126 | 127 | /// Truncates the stack to the last frame. 128 | /// Returns `true` if the stack can not be unwound further. 129 | #[inline] 130 | pub fn unwind_frame(&mut self) -> bool { 131 | self.stack.truncate(self.frame_index() + 1); 132 | return self.frames.len() > 1; 133 | } 134 | 135 | /// returns a copy of the `Slot` of a local variable on the stack. 136 | pub fn local_slot(&mut self, index: usize) -> Slot { 137 | let local_index = self.frame_index() + index + 1; 138 | 139 | // a little bit of shuffling involved 140 | // I know that something better than this can be done 141 | let slot = self.swap(local_index, Tagged::not_init()).slot(); 142 | let copy = slot.clone(); 143 | mem::drop(self.swap(local_index, Tagged::new(slot))); 144 | 145 | return copy; 146 | } 147 | 148 | pub fn local_ref(&mut self, index: usize) -> Rc> { 149 | let local_index = self.frame_index() + index + 1; 150 | 151 | // a little bit of shuffling involved 152 | // I know that something better than this can be done 153 | let slot = self 154 | .swap(local_index, Tagged::not_init()) 155 | .slot() 156 | .reference(); 157 | let copy = slot.clone(); 158 | mem::drop(self.swap(local_index, Tagged::new(Slot::Ref(slot)))); 159 | 160 | return copy; 161 | } 162 | 163 | /// Returns a copy of the `Data` stored in a local variable on the stack. 164 | pub fn local_data(&mut self, index: usize) -> Data { 165 | let local_index = self.frame_index() + index + 1; 166 | 167 | // a little bit of shuffling involved 168 | // I know that something better than this can be done 169 | let data = self.swap(local_index, Tagged::not_init()).slot().data(); 170 | let copy = data.clone(); 171 | mem::drop(self.swap(local_index, Tagged::new(Slot::Data(data)))); 172 | 173 | return copy; 174 | } 175 | 176 | /// Sets a local - note that this function doesn't do much. 177 | /// It's a simple swap-and-drop. 178 | /// If a new local is being declared, 179 | /// it's literally a bounds-check and no-op. 180 | pub fn set_local(&mut self, index: usize) { 181 | let local_index = self.frame_index() + index + 1; 182 | 183 | match self.stack.len() - 1 { 184 | // local is already in the correct spot; we declare it 185 | n if n == local_index => return, 186 | n if n < local_index => { 187 | // println!("{} < {}", self.stack.len() - 1, local_index); 188 | unreachable!("Can not set local that is not yet on stack"); 189 | } 190 | _ => (), 191 | } 192 | 193 | // get the old local 194 | let slot = self.swap(local_index, Tagged::not_init()).slot(); 195 | 196 | // replace the old value with the new one if on the heap 197 | let tagged = match slot { 198 | // if it's data we just grab it 199 | Slot::Data(_) => self.stack.pop().unwrap(), 200 | // if it is on the heap, we replace in the old value 201 | Slot::Ref(ref cell) => { 202 | // TODO: check types? 203 | mem::drop(cell.replace(self.pop_data())); 204 | Tagged::new(slot) 205 | }, 206 | // if it's anything else, we're sad. 207 | Slot::Frame => unreachable!("Expected data, found frame"), 208 | Slot::Suspend(_) => unreachable!("Expected data, found *suspended* frame buried deep in the stack, which makes even less sense, because this should be a local variable"), 209 | Slot::NotInit => unreachable!("Expected data, found data. Wait! It's unitialized? Maybe this is allowed?"), 210 | }; 211 | 212 | mem::drop(self.swap(local_index, tagged)) 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /passerine-qualm/src/heap/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::Slot; 2 | 3 | pub mod pointer; 4 | pub mod range_set; 5 | 6 | pub use pointer::Pointer; 7 | use range_set::RangeSet; 8 | 9 | #[derive(Debug)] 10 | pub struct Heap { 11 | data: Vec, 12 | free: RangeSet, 13 | } 14 | 15 | impl Heap { 16 | /// Constructs new empty heap. 17 | pub fn new() -> Heap { 18 | Heap { 19 | data: vec![], 20 | free: RangeSet::new(), 21 | } 22 | } 23 | 24 | /// Dumps a representation of the heap to stdout. 25 | /// Useful for general debugging. 26 | pub fn draw_free(&self) { 27 | print!("|"); 28 | let mut old = 0; 29 | let mut unused = 0; 30 | for (key, value) in self.free.ranges.iter() { 31 | print!("{}", "_".repeat(key.to_usize() - old)); 32 | print!("{}", "X".repeat(*value)); 33 | unused += value; 34 | old = key.to_usize(); 35 | } 36 | print!("{}", "_".repeat(self.free.capacity - old)); 37 | println!("|"); 38 | 39 | println!("==== INFO ===="); 40 | println!("heap size: {} bytes", self.data.len() * 8); 41 | println!("total slots: {} slots", self.data.len()); 42 | println!("disjoint ranges: {} slots", self.free.ranges.len()); 43 | let pct = (unused as f64 / self.free.capacity as f64) * 100.0; 44 | println!( 45 | "fragmentation: {} / {} = {:.2}%", 46 | unused, self.free.capacity, pct 47 | ); 48 | } 49 | 50 | /// Allocate a pointer of a given size. 51 | /// Returns the smallest first allocation that will fit the pointer. 52 | /// 53 | /// # Safety 54 | /// The allocated pointer will point to garbage data. 55 | /// This call must immediately be followed by a call to [`Heap::write`]. 56 | pub unsafe fn alloc(&mut self, slots: usize) -> Pointer { 57 | let (pointer, extra_capacity) = self.free.mark_first(slots); 58 | 59 | // increase the size of the allocation if needed. 60 | self.data 61 | .extend((0..extra_capacity).map(|_| unsafe { Slot::zero() })); 62 | return pointer; 63 | } 64 | 65 | /// Reallocates an allocation to a larger size 66 | /// Tries to reallocate in place, but moves the allocation if needed. 67 | /// 68 | /// # Safety 69 | /// All previously allocated data will be present at the start of the new 70 | /// allocation. For example, if you have an allocation `*x = ABC` of len 71 | /// 3, and reallocate to len 5, the new allocation will be `*x = ABC__`. 72 | /// 73 | /// Likewise, if you decrease the size of the allocation, data will be 74 | /// truncated. So `*x = ABC` of len 3 reallocated to len 1 would be `*x 75 | /// = A`. 76 | /// 77 | /// Like when using `Heap::alloc`, this call must be immediately be followed 78 | /// by a call to [`Heap::write`] to fill the uninitialized portion of 79 | /// the new array. 80 | pub unsafe fn realloc( 81 | &mut self, 82 | pointer: Pointer, 83 | old: usize, 84 | new: usize, 85 | ) -> Pointer { 86 | assert!(pointer.is_owned()); 87 | 88 | if new > old { 89 | // try allocation continiously 90 | let tail = pointer.add(old as u64); 91 | if self.free.is_free(tail, new - old) { 92 | // increase the size of the current allocation 93 | self.free.mark_smaller(tail.to_idx(), new - old); 94 | return pointer; 95 | } 96 | 97 | // TODO: free before reallocation might open up space before? 98 | // reallocate new larger allocation, copy over data. 99 | let new_pointer = self.alloc(new); 100 | for slot in 0..old { 101 | self.data.swap( 102 | new_pointer.to_idx().to_usize() + slot, 103 | pointer.to_idx().to_usize() + slot, 104 | ); 105 | } 106 | // and free old small allocation 107 | self.free(pointer, old); 108 | return new_pointer; 109 | } else if old > new { 110 | // free back half of allocation 111 | self.free(pointer.add(new as u64), old - new); 112 | } 113 | 114 | // they're equal, so do nothing 115 | return pointer; 116 | } 117 | 118 | // Reads a single slot relative to a pointer. 119 | pub fn read_slot(&self, pointer: Pointer, slot: usize) -> &Slot { 120 | &self.data[pointer.to_idx().to_usize() + slot] 121 | } 122 | 123 | // Reads a range of data. 124 | pub fn read(&self, pointer: Pointer, slots: usize) -> &[Slot] { 125 | let start = pointer.to_idx().to_usize() as usize; 126 | &self.data[start..(start + slots)] 127 | } 128 | 129 | // TODO: figure out apis for reading and writing 130 | 131 | pub fn write_slot(&self, pointer: Pointer, slots: usize) -> () { todo!() } 132 | 133 | pub fn write(&mut self, pointer: Pointer, item: &mut [Slot]) -> Option<()> { 134 | // can't write to a pointer we don't own! make a copy first. 135 | if !pointer.is_owned() { 136 | return None; 137 | } 138 | } 139 | 140 | pub fn free(&mut self, pointer: Pointer, slots: usize) { 141 | assert!(pointer.is_owned()); 142 | let unneeded_capacity = self.free.free(pointer, slots); 143 | self.data.truncate(self.data.len() - unneeded_capacity); 144 | } 145 | } 146 | 147 | #[cfg(test)] 148 | pub mod tests { 149 | use std::collections::BTreeMap; 150 | 151 | use super::*; 152 | 153 | fn random_alloc_size(rng: &mut attorand::Rng) -> usize { 154 | rng.next_byte() as usize + 1 155 | } 156 | 157 | const STRESS_ITER: usize = 1000; 158 | 159 | #[test] 160 | pub fn stress_test_heap() { 161 | let mut heap = Heap::new(); 162 | let mut pointers = BTreeMap::new(); 163 | let mut rng = attorand::Rng::new_default(); 164 | 165 | for i in 0..STRESS_ITER { 166 | if i % 100 == 0 { 167 | println!("{}", i); 168 | } 169 | let size = random_alloc_size(&mut rng); 170 | // SAFETY: data is never read 171 | let pointer = unsafe { heap.alloc(size) }; 172 | pointers.insert(i, (pointer, size)); 173 | 174 | let index = rng.next_u64_max((pointers.len() - 1) as u64) as usize; 175 | if rng.next_bool() { 176 | let (index, (to_modify, old_size)) = 177 | pointers.iter().nth(index).unwrap(); 178 | let index = *index; 179 | 180 | if rng.next_bool() { 181 | let new_size = random_alloc_size(&mut rng); 182 | // SAFETY: data is never read 183 | let pointer = unsafe { 184 | heap.realloc(*to_modify, *old_size, new_size) 185 | }; 186 | pointers.insert(index, (pointer, new_size)); 187 | } else { 188 | heap.free(*to_modify, *old_size); 189 | pointers.remove(&index); 190 | } 191 | } 192 | } 193 | 194 | heap.draw_free(); 195 | } 196 | 197 | #[test] 198 | pub fn stress_test_native() { 199 | let mut rng = attorand::Rng::new_default(); 200 | let mut pointers = BTreeMap::new(); 201 | 202 | for i in 0..STRESS_ITER { 203 | let size = random_alloc_size(&mut rng); 204 | let pointer = vec![0; size]; 205 | pointers.insert(i, (pointer, size)); 206 | 207 | let index = rng.next_u64_max((pointers.len() - 1) as u64) as usize; 208 | if rng.next_bool() { 209 | let (index, _) = pointers.iter().nth(index).unwrap(); 210 | let index = *index; 211 | 212 | if rng.next_bool() { 213 | let new_size = random_alloc_size(&mut rng); 214 | let pointer = vec![0; new_size]; 215 | pointers.insert(index, (pointer, new_size)); 216 | } else { 217 | pointers.remove(&index); 218 | } 219 | } 220 | } 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 |

6 |

The Passerine Programming Language

7 |

Made with ♡ by Isaac Clayton and the Passerine Community – a cornerstone of the Veritable Computation Initiative.

8 |

9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |
20 |

21 | 22 | ## Why Passerine? 23 | 24 | [Passerine](https://www.passerine.io) is a small, concise, extensible functional scripting language, powered by a VM written in [Rust](https://www.rust-lang.org). Here's a small taste: 25 | 26 |

27 | 28 | 29 | 30 |

31 | 32 | Passerine has roots in Scheme and ML-flavored languages: it's the culmination of everything I expect from a programming language, including the desire to keep everything as minimalistic and concise as possible. At its core, Passerine is lambda-calculus with pattern-matching, structural types, fiber-based concurrency, and syntactic extension. 33 | 34 | ### Who started this? 35 | 36 | Passerine was started by [Isaac Clayton](https://github.com/slightknack) at the end of 2019. In August of 2022, Isaac handed over the maintenece of Passerine and its community to his friend [Ben Siraphob](https://github.com/siraben). 37 | 38 | A number of people have offered feedback and suggestions from time to time. Huge thanks to 39 | [Raul](https://github.com/spaceface777), 40 | [Hamza](https://github.com/hhhapz), 41 | [Lúcás](https://github.com/cronokirby), 42 | [Anton](https://github.com/jesyspa/), 43 | [Yasser](https://github.com/realnegate), 44 | [Shaw](https://github.com/shawsumma)†, 45 | [Ben](https://github.com/siraben), 46 | [Plecra](https://github.com/plecra), 47 | [IFcoltransG](https://github.com/IFcoltransG), 48 | [Jack](https://github.com/nivpgir), 49 | Xal, 50 | [Zack](https://github.com/zack466), 51 | [Mahmoud](https://github.com/fuzzypixelz) 52 | and many others! 53 | 54 | > Shaw is the developer of *MiniVM*, a portable cross-platform runtime. The eventual goal is to retire Passerine's current virtual machine, and adopt MiniVM Assembly (VASM), then later LLVM IR, as first-class targets of the compiler. 55 | 56 | ## An Overview 57 | We've recently moved the Overview to a separate website, the [Passerine Codex](https://codex.passerine.io/core/syntax.html), which can be read online. 58 | 59 | > **Note**: Passerine is a *work in progress*: features mentioned in this overview may be unimplemented or subject to change. 60 | 61 | ## FAQ 62 | 63 | **Q:** Is Passerine ready for production use? 64 | 65 | **A:** Not yet! Passerine is still in early stages of development, with frequent breaking changes. 66 | 67 | **Q:** Is Passerine statically typed? 68 | 69 | **A:** Currently Passerine is strongly and dynamically¹ typed (technically structurally typed). This is partially out of necessity – Types are defined by patterns, and patterns can be where predicated. However, I've been doing a lot of research into Hindley-Milder type systems, and the various extensions that can be applied to them. 70 | 71 | I'm working towards making a compile-time type-checker for the language, based on Hindley-Milner type inference. With this system in place, I can make some assumptions to speed up the interpreter further and perhaps monomorphize/generate LLVM IR / WASM. 72 | 73 | This type checker is actually the target of the next release, so stay tuned! 74 | 75 | **Q:** What about algebraic effects and kind-based macros? 76 | 77 | **A:** I'm interested in eventually adding both these things to the language, but first I need to implement a nice type-checker and do some more research. Algebraic Effects would fill the design space of fibers, and kind based macros would provide a more solid base for passerine's macro system. Got any fresh language features you think would complement Passerine's design philosophy? Reach out! 78 | 79 | **Q:** What is vaporization memory management? 80 | 81 | **A:** When I was first designing Passerine, I was big into automatic compile-time memory management. Currently, there are a few ways to do this: from Rust's borrow-checker, to µ-Mitten's Proust ASAP, to Koka's Perceus, there are a lot of new and exciting ways to approach this problem. 82 | 83 | Vaporization is an automatic memory management system that allows for *Functional but in Place* style programming. For vaporization to work, three invariants must hold: 84 | 85 | 1. All functions params are passed by value via a copy-on-write reference. This means that only the lifetimes of the returned objects need to be preserved, all others will be deleted when they go out of scope. 86 | 2. A form of SSA is performed, where the last usage of any value is not a copy of that value. 87 | 3. All closure references are immutable copies of a value. These copies may be reference-counted in an acyclical manner. 88 | 89 | With these invariants in place, vaporization ensures two things: 90 | 91 | 1. Values are only alive where they are still *useful*. 92 | 2. Code may be written in a functional style, but all mutations occur in-place as per rule 2. 93 | 94 | What's most interesting is that this system requires minimal interference from the compiler when used in conjunction with a VM. All the compiler has to do is annotate the last usage of the value of any variables; the rest can be done automatically and very efficiently at runtime. 95 | 96 | Why not use this? Mainly because of rule 3: 'closure references are immutable'. Passerine is pass-by-value, but currently allows mutation in the current scope a la let-style redefinition. But this is subject to change; and once it does, it's vaporization all the way, baby! 97 | 98 | **Q:** Aren't there already enough programming languages? 99 | 100 | **A:** Frankly, I think we've barely *scratched* the surface of programming language design. To say that Programming Language Design is saturated and at a local maxima is to not understand the nature of software development. Passerine is largely a test as to whether I can build a modern compiler pipeline. But what I'm even more interested in is the tooling that surrounds development environments. 101 | 102 | Case in point: text-based entry for programming languages has been around forever because it's fast. However, it's not always semantically correct. The number of correct programs is an infinity smaller than the number of possible text files. Yet it's still possible to make text-based entry systems that ensure semantic correctness while encouraging exploration. In the future, we need to develop new tools that more closely blur the line between language and environment. Pharo is a step in the right direction, as are Unison and similar efforts. 103 | 104 | I'd like to focus more on this in the future. An interesting project would be an editor/environment like Pharo/Unison for a small minimal language, like Scheme, or perhaps even Passerine. 105 | 106 | ## Installation 107 | 108 | Passerine is still very much so a work in progress. We've done a lot, but there's still a so much more to do! 109 | 110 | For you pioneers out there, The best way to get a feel for Passerine is to install *Aspen*¹, Passerine's package manager and CLI. If you use a *nix-style² system, run³⁴: 111 | 112 | ```bash 113 | cargo install aspen 114 | ``` 115 | 116 | This will always install the latest *stable* version of Passerine. To get a feel for the current state of development hell, check out a local copy for your own enjoyment: 117 | 118 | ``` 119 | git clone https://github.com/vrtbl/passerine 120 | ``` 121 | 122 | > 1. If you're having trouble getting started, reach out on the community [Discord server](https://discord.gg/yMhUyhw). 123 | > 2. Tested on Arch (btw) and macOS. 124 | > 3. Now tested on Windows™! 125 | > 4. In the future, we plan to distribute prebuilt binaries, but for now, a Rust install is required. 126 | 127 | ## Contributing 128 | 129 | Contributions are welcome! 130 | Read our [Contribution Guide](https://github.com/vrtbl/passerine/blob/master/CONTRIBUTING.md) 131 | and join the [Discord server](https://discord.gg/yMhUyhw) 132 | to get started! 133 | -------------------------------------------------------------------------------- /passerine/tests/fledgling.rs: -------------------------------------------------------------------------------- 1 | ///! Snippet tests for the passerine compiler pipeline as a whole. 2 | use std::{collections::HashMap, fs, path::PathBuf, rc::Rc}; 3 | 4 | use passerine::*; 5 | 6 | /// Represents specific success/failure modes of a snippet test. 7 | #[derive(Debug, PartialEq, Eq)] 8 | pub enum Outcome { 9 | Success, 10 | Syntax, 11 | Trace, 12 | } 13 | 14 | impl Outcome { 15 | pub fn parse(outcome: &str) -> Outcome { 16 | match outcome { 17 | s if s == "success" => Outcome::Success, 18 | s if s == "syntax" => Outcome::Syntax, 19 | t if t == "trace" => Outcome::Trace, 20 | invalid => { 21 | println!("invalid: '{}'", invalid); 22 | panic!("invalid outcome in strat heading"); 23 | } 24 | } 25 | } 26 | } 27 | 28 | /// Represents what part of the compiler a snippet tests. 29 | #[derive(Debug, PartialEq, Eq)] 30 | pub enum Action { 31 | Lex, 32 | Parse, 33 | Read, 34 | Desugar, 35 | Hoist, 36 | Gen, 37 | Run, 38 | } 39 | 40 | impl Action { 41 | pub fn parse(action: &str) -> Action { 42 | match action { 43 | l if l == "lex" => Action::Lex, 44 | p if p == "parse" => Action::Parse, 45 | r if r == "read" => Action::Read, 46 | d if d == "desugar" => Action::Desugar, 47 | d if d == "hoist" => Action::Hoist, 48 | g if g == "gen" => Action::Gen, 49 | r if r == "run" => Action::Gen, // TODO: actually run code! 50 | invalid => { 51 | println!("invalid: '{}'", invalid); 52 | panic!("invalid action in strat heading"); 53 | } 54 | } 55 | } 56 | } 57 | 58 | /// Represents a test strategy for executing a snippet, 59 | /// Found at the top of each file. 60 | #[derive(Debug)] 61 | pub struct TestStrat { 62 | /// How to run the test. 63 | action: Action, 64 | /// The expected outcome. 65 | outcome: Outcome, 66 | /// Optional data to check against. 67 | /// Should only be used with Action::Run 68 | expect: Option, 69 | } 70 | 71 | impl TestStrat { 72 | /// Uses a heading to construct a test strat 73 | pub fn heading(heading: HashMap) -> TestStrat { 74 | let mut outcome = None; 75 | let mut action = None; 76 | let mut expect = None; 77 | 78 | for (strat, result) in heading.iter() { 79 | match strat { 80 | o if o == "outcome" => outcome = Some(Outcome::parse(result)), 81 | a if a == "action" => action = Some(Action::parse(result)), 82 | e if e == "expect" => { 83 | // TODO: implement expectations. 84 | // println!("Warning, expectations are not implemented"); 85 | // expect = 86 | // { 87 | // let (ast, _symbols) = 88 | // compiler::parse(Source::source(result)) 89 | // .expect("Could not parse result field"); 90 | 91 | // use construct::tree::{ 92 | // Base, 93 | // AST, 94 | // }; 95 | 96 | // if let AST::Base(Base::Block(ref b)) = ast.item { 97 | // if let AST::Base(Base::Lit(ref l)) = 98 | // b.get(0).expect("Expected an 99 | // expression").item { 100 | // assert_eq!(l, ) 101 | // } 102 | // } 103 | 104 | // dbg!(ast); 105 | // } 106 | // todo!(); 107 | } 108 | invalid => { 109 | println!("invalid: '{}'", invalid); 110 | panic!("invalid strat in strat heading"); 111 | } 112 | } 113 | } 114 | 115 | TestStrat { 116 | outcome: outcome.expect("no outcome provided"), 117 | action: action.expect("no action provided"), 118 | expect, 119 | } 120 | } 121 | 122 | /// Parses the Test Strat from a given snippet. 123 | pub fn snippet(source: &Rc) -> TestStrat { 124 | let mut heading = HashMap::new(); 125 | let lines = source.contents.lines(); 126 | 127 | // build up a list of key-value pairs 128 | for line in lines { 129 | if line.len() <= 1 || &line[0..1] != "#" { 130 | break; 131 | }; 132 | 133 | let spliced = line[1..].trim().split(":").collect::>(); 134 | if spliced.len() <= 1 { 135 | panic!("Missing colon in test strat heading") 136 | } 137 | 138 | let strat = spliced[0]; 139 | let result = spliced[1..].join(":"); 140 | if heading 141 | .insert(strat.trim().to_string(), result.trim().to_string()) 142 | .is_some() 143 | { 144 | panic!("Key present twice in test strat heading"); 145 | } 146 | } 147 | 148 | return TestStrat::heading(heading); 149 | } 150 | } 151 | 152 | fn outcome(t: Result) -> Outcome { 153 | if let Err(e) = t { 154 | eprintln!("{}", e); 155 | Outcome::Syntax 156 | } else { 157 | Outcome::Success 158 | } 159 | } 160 | 161 | fn snippet_outcome(source: Rc, strat: &TestStrat) -> Outcome { 162 | let result = match strat.action { 163 | Action::Lex => return outcome(compiler::lex(source)), 164 | Action::Parse => return outcome(compiler::parse(source)), 165 | Action::Read => return outcome(compiler::read(source)), 166 | Action::Desugar => return outcome(compiler::desugar(source)), 167 | Action::Hoist => return outcome(compiler::hoist(source)), 168 | Action::Gen => return outcome(compiler::gen(source)), 169 | Action::Run => compiler::gen(source), 170 | }; 171 | 172 | let lambda = match result { 173 | Ok(l) => l, 174 | Err(_) => return Outcome::Syntax, 175 | }; 176 | 177 | println!("{}", &lambda); 178 | println!( 179 | "{}", 180 | if let Data::Lambda(s) = &lambda.constants[0] { 181 | s 182 | } else { 183 | todo!() 184 | } 185 | ); 186 | 187 | let mut Fiber = Fiber::init(Closure::wrap(lambda)); 188 | 189 | let run_outcome = match Fiber.run() { 190 | Ok(()) => { 191 | if let Some(expected) = &strat.expect { 192 | let top = Fiber.stack.pop_data(); 193 | if expected != &top { 194 | println!("Top: {}", top); 195 | println!("Expected: {}", expected); 196 | panic!("Top stack data does not match") 197 | } 198 | } 199 | return Outcome::Success; 200 | } 201 | Err(e) => { 202 | eprintln!("{}", e); 203 | Outcome::Trace 204 | } 205 | }; 206 | 207 | return run_outcome; 208 | } 209 | 210 | fn test_snippet(source: Rc, strat: &TestStrat) { 211 | let outcome = snippet_outcome(source, strat); 212 | if outcome != strat.outcome { 213 | println!("expected outcome {:?}", strat.outcome); 214 | println!("actual outcome {:?}", outcome); 215 | panic!("test failed, outcomes are not the same"); 216 | } 217 | } 218 | 219 | fn snippets(dir: &str) { 220 | let paths = fs::read_dir(dir) 221 | .expect("You must be in the base passerine directory, snippets in ./tests/snippets"); 222 | 223 | let mut to_run: Vec = vec![]; 224 | for path in paths { 225 | to_run.push(path.expect("Could not read path").path()) 226 | } 227 | 228 | let mut counter = 0; 229 | println!("\nRunning {} snippet test(s)...", to_run.len()); 230 | 231 | // TODO: subdirectories of tests 232 | while let Some(path) = to_run.pop() { 233 | println!("test {}: {}...", counter, path.display()); 234 | 235 | let source = Source::path(&path).expect("Could not get snippet source"); 236 | let test_strat = TestStrat::snippet(&source); 237 | 238 | test_snippet(source, &test_strat); 239 | counter += 1; 240 | } 241 | 242 | println!("All tests passed!\n"); 243 | } 244 | 245 | #[test] 246 | fn test_snippets() { 247 | snippets("./tests/snippets") 248 | } 249 | -------------------------------------------------------------------------------- /passerine-common/src/lambda.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use crate::{data::Data, number::build_number, opcode::Opcode, span::Span}; 4 | 5 | /// Represents a variable visible in the current scope. 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | pub enum Captured { 8 | /// The index on the stack if the variable is local to the current scope. 9 | Local(usize), 10 | /// The index of the upvalue in the enclosing scope. 11 | Nonlocal(usize), 12 | } 13 | 14 | /// Represents a single interpretable chunk of bytecode, 15 | /// think a function. 16 | #[derive(Debug, Clone, PartialEq)] 17 | pub struct Lambda { 18 | // TODO: make this a list of variable names 19 | // So structs can be made, and state preserved in the repl. 20 | /// Number of variables declared in this scope. 21 | pub decls: usize, 22 | /// Each byte is an opcode or a number-stream. 23 | pub code: Vec, 24 | /// Each usize indexes the bytecode op that begins each line. 25 | pub spans: Vec<(usize, Span)>, 26 | /// Number-stream indexed, used to load constants. 27 | pub constants: Vec, 28 | /// List of positions of locals in the scope where this lambda is defined, 29 | /// indexes must be gauranteed to be data on the heap. 30 | pub captures: Vec, 31 | // TODO: delete FFI 32 | // / List of FFI functions (i.e. Rust functions) 33 | // / that can be called from this function. 34 | // pub ffi: Vec, 35 | // TODO: add effects 36 | // pub effects: Vec, 37 | } 38 | 39 | impl Lambda { 40 | /// Creates a new empty `Lambda` to be filled. 41 | pub fn empty() -> Lambda { 42 | Lambda { 43 | decls: 0, 44 | code: vec![], 45 | spans: vec![], 46 | constants: vec![], 47 | captures: vec![], 48 | // ffi: vec![], 49 | } 50 | } 51 | 52 | /// Constructs a number of bytecode arguments, 53 | /// ensuring each is within a specific bound. 54 | /// If any bounds are violated, we return `None`. 55 | pub fn args_safe(&self, index: usize, within: &[usize]) -> Option<(Vec, usize)> { 56 | let mut offset = 0; 57 | let mut numbers = vec![]; 58 | 59 | for bound in within.iter() { 60 | let (arg, consumed) = build_number(&self.code[index + offset..]); 61 | if arg >= *bound { 62 | return None; 63 | } 64 | numbers.push(arg); 65 | offset += consumed; 66 | } 67 | 68 | return Some((numbers, offset)); 69 | } 70 | 71 | pub fn bounds(&self, opcode: Opcode) -> Vec { 72 | match opcode { 73 | Opcode::Con => vec![self.constants.len()], 74 | Opcode::NotInit => vec![], 75 | Opcode::Del => vec![], 76 | Opcode::FFICall => panic!(), 77 | Opcode::Copy => vec![], 78 | // Opcode::Capture => vec![self.decls], 79 | 80 | // TODO: correct bounds check? 81 | Opcode::Save => vec![self.decls], 82 | Opcode::SaveCap => vec![self.captures.len()], 83 | Opcode::Load => vec![self.decls], 84 | Opcode::LoadCap => vec![self.captures.len()], 85 | Opcode::Call => vec![], 86 | Opcode::Return => vec![self.decls], 87 | // TODO: correct bounds check? 88 | Opcode::Closure => vec![self.constants.len()], 89 | Opcode::Print => vec![], 90 | Opcode::Label => vec![], 91 | Opcode::Tuple => vec![usize::MAX], // TODO: stricter bounds 92 | Opcode::UnData => vec![], 93 | Opcode::UnLabel => vec![], 94 | Opcode::UnTuple => vec![usize::MAX], // TODO: stricter bounds 95 | Opcode::Noop => vec![], 96 | e => panic!("not implemented {:?}", e), 97 | } 98 | } 99 | 100 | /// NOTE: WIP, do not use. 101 | /// Statically verifies some bytecode safely, 102 | /// By ensuring bytecode ops are within bounds, 103 | /// As well as the arguments those ops take. 104 | /// Returns `false` if the bytecode is invalid. 105 | pub fn verify(&self) -> bool { 106 | // go through each opcode 107 | // check the number of arguments 108 | // check that the arguments are valid 109 | let mut index = 0; 110 | 111 | while index < self.code.len() { 112 | // safely decode an opcode 113 | let opcode = match Opcode::from_byte_safe(self.code[index]) { 114 | Some(o) => o, 115 | None => { 116 | return false; 117 | } 118 | }; 119 | 120 | index += 1; 121 | let bounds = self.bounds(opcode); 122 | let args_result = self.args_safe(index, &bounds); 123 | 124 | index += match args_result { 125 | Some((_args, consumed)) => consumed, 126 | None => { 127 | return false; 128 | } 129 | } 130 | } 131 | 132 | return true; 133 | } 134 | 135 | /// Emits an opcode as a byte. 136 | pub fn emit(&mut self, op: Opcode) { 137 | self.code.push(op as u8) 138 | } 139 | 140 | /// Emits a series of bytes. 141 | pub fn emit_bytes(&mut self, bytes: &mut Vec) { 142 | self.code.append(bytes) 143 | } 144 | 145 | /// Emits a span, should be called before an opcode is emmited. 146 | /// This function ties opcodes to spans in source. 147 | /// See index_span as well. 148 | pub fn emit_span(&mut self, span: &Span) { 149 | self.spans.push((self.code.len(), span.clone())) 150 | } 151 | 152 | /// Removes the last emitted byte. 153 | pub fn demit(&mut self) { 154 | self.code.pop(); 155 | } 156 | 157 | /// Given some data, this function adds it to the constants table, 158 | /// and returns the data's index. 159 | /// The constants table is push only, so constants are identified by their 160 | /// index. The resulting usize can be split up into a number byte 161 | /// stream, and be inserted into the bytecode. 162 | pub fn index_data(&mut self, data: Data) -> usize { 163 | match self.constants.iter().position(|d| d == &data) { 164 | Some(d) => d, 165 | None => { 166 | self.constants.push(data); 167 | self.constants.len() - 1 168 | } 169 | } 170 | } 171 | 172 | /// Look up the nearest span at or before the index of a specific bytecode 173 | /// op. 174 | pub fn index_span(&self, index: usize) -> Span { 175 | let mut best = None; 176 | 177 | for (i, span) in self.spans.iter() { 178 | if i > &index { 179 | break; 180 | } 181 | best = Some(span); 182 | } 183 | 184 | return best.unwrap().clone(); 185 | } 186 | 187 | // /// Adds a ffi function to the ffi table, 188 | // /// without checking for duplicates. 189 | // /// The `Compiler` ensures that functions are valid 190 | // /// and not duplicated during codegen. 191 | // pub fn add_ffi(&mut self, function: FFIFunction) -> usize { 192 | // self.ffi.push(function); 193 | // self.ffi.len() - 1 194 | // } 195 | } 196 | 197 | impl fmt::Display for Lambda { 198 | /// Dump a human-readable breakdown of a `Lambda`'s bytecode. 199 | /// Including constants, captures, and variables declared. 200 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> fmt::Result { 201 | writeln!(f, "Dumping Constants:")?; 202 | for constant in self.constants.iter() { 203 | writeln!(f, "{:?}", constant)?; 204 | } 205 | 206 | writeln!(f, "Dumping Captures:")?; 207 | for capture in self.captures.iter() { 208 | writeln!(f, "{:?}", capture)?; 209 | } 210 | 211 | writeln!(f, "Dumping Variables: {:?}", self.decls)?; 212 | 213 | writeln!(f, "Dumping Bytecode:")?; 214 | writeln!(f, "Inst\tArgs")?; 215 | 216 | let mut index = 0; 217 | 218 | while index < self.code.len() { 219 | // safely decode an opcode 220 | let opcode = match Opcode::from_byte_safe(self.code[index]) { 221 | Some(o) => o, 222 | None => { 223 | writeln!(f, "Invalid Opcode at index {}", index)?; 224 | break; 225 | } 226 | }; 227 | 228 | write!(f, "{:?}\t", opcode)?; 229 | 230 | index += 1; 231 | let bounds = self.bounds(opcode); 232 | let args_result = self.args_safe(index, &bounds); 233 | 234 | let (args, consumed) = match args_result { 235 | Some((a, c)) => (a, c), 236 | None => { 237 | writeln!(f, "\nInvalid Opcode argument at index {}", index)?; 238 | break; 239 | } 240 | }; 241 | 242 | index += consumed; 243 | writeln!( 244 | f, 245 | "{}", 246 | args.iter() 247 | .map(|n| n.to_string()) 248 | .collect::>() 249 | .join("\t") 250 | )?; 251 | } 252 | 253 | return fmt::Result::Ok(()); 254 | } 255 | } 256 | -------------------------------------------------------------------------------- /Logotype.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /passerine-common/src/span.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fmt::{self, Debug, Display, Formatter}, 3 | ops::Add, 4 | rc::Rc, 5 | usize, 6 | }; 7 | 8 | use crate::source::Source; 9 | 10 | /// A `Span` refers to a section of a source, 11 | /// much like a `&str`, but with a reference to a `Source` 12 | /// rather than a `String`. A `Span` is meant to be paired 13 | /// with other datastructures, to be used during error 14 | /// reporting. 15 | #[derive(Clone, Eq, PartialEq)] 16 | pub struct Span { 17 | source: Rc, 18 | offset: usize, 19 | length: usize, 20 | } 21 | 22 | impl Span { 23 | /// Create a new `Span` from an offset with a length. 24 | /// All `Span`s have access to the `Source` from whence 25 | /// they came, So they can't be misinterpreted or 26 | /// miscombined. 27 | pub fn new(source: &Rc, offset: usize, length: usize) -> Span { 28 | Span { 29 | source: Rc::clone(source), 30 | offset, 31 | length, 32 | } 33 | } 34 | 35 | /// A `Span` that points at a specific point in the 36 | /// source. Has a length of `0`. 37 | pub fn point(source: &Rc, offset: usize) -> Span { 38 | Span { 39 | source: Rc::clone(source), 40 | offset, 41 | length: 0, 42 | } 43 | } 44 | 45 | /// Return the index of the end of the `Span`. 46 | pub fn end(&self) -> usize { 47 | self.offset + self.length 48 | } 49 | 50 | #[allow(clippy::len_without_is_empty)] 51 | pub fn len(&self) -> usize { 52 | self.length 53 | } 54 | 55 | /// Creates a new `Span` which spans the space of the 56 | /// previous two. ```plain 57 | /// hello this is cool 58 | /// ^^^^^ | Span a 59 | /// ^^ | Span b 60 | /// ^^^^^^^^^^^^^ | combined 61 | /// ``` 62 | pub fn combine(a: &Span, b: &Span) -> Span { 63 | if a.source != b.source { 64 | panic!("Can't combine two Spans with separate sources"); 65 | } 66 | 67 | let offset = a.offset.min(b.offset); 68 | let end = a.end().max(b.end()); 69 | let length = end - offset; 70 | 71 | return Span::new(&a.source, offset, length); 72 | } 73 | 74 | /// Combines a set of `Span`s (think fold-left over 75 | /// `Span::combine`). If the vector of spans passed 76 | /// in is empty, this method panics. 77 | pub fn join(mut spans: Vec) -> Option { 78 | let mut combined = spans.pop()?; 79 | 80 | while let Some(span) = spans.pop() { 81 | combined = Span::combine(&combined, &span) 82 | } 83 | 84 | return Some(combined); 85 | } 86 | 87 | /// Returns the contents of a `Span`. 88 | /// This indexes into the source file, 89 | /// so if the `Span` is along an invalid byte boundary 90 | /// or is empty, the program will panic. 91 | pub fn contents(&self) -> String { 92 | self.source.as_ref().contents[self.offset..self.end()].to_string() 93 | } 94 | 95 | pub fn lines(&self) -> Vec { 96 | let full_source = &self.source.as_ref().contents; 97 | let lines: Vec<_> = full_source.split('\n').collect(); 98 | let start_line = self.line(self.offset); 99 | let end_line = self.line(self.end()); 100 | let slice = lines[start_line..=end_line] 101 | .iter() 102 | .map(|s| s.to_string()) 103 | .collect(); 104 | // dbg!(start_line); 105 | // dbg!(end_line); 106 | return slice; 107 | } 108 | 109 | pub fn path(&self) -> String { 110 | self.source.clone().path.to_string_lossy().to_string() 111 | } 112 | 113 | pub fn line(&self, index: usize) -> usize { 114 | let lines = self.source.contents[..index].split('\n').count(); 115 | return lines.saturating_sub(1); 116 | } 117 | 118 | pub fn col(&self, index: usize) -> usize { 119 | let lines = &self.source.contents[..index] 120 | .split('\n') 121 | .last() 122 | .unwrap_or("") 123 | .chars() 124 | .count(); 125 | return *lines; 126 | } 127 | 128 | pub fn format(&self) -> FormattedSpan { 129 | FormattedSpan { 130 | path: self.path(), 131 | start: self.line(self.offset), 132 | lines: self.lines(), 133 | start_col: self.col(self.offset), 134 | end_col: self.col(self.end()), 135 | } 136 | } 137 | } 138 | 139 | impl Debug for Span { 140 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 141 | f.debug_struct("Span") 142 | .field("contents", &self.contents()) 143 | .field("start", &self.offset) 144 | .field("end", &self.end()) 145 | .finish() 146 | } 147 | } 148 | 149 | // TODO: tests 150 | // TODO: this can be vastly simplified 151 | impl Display for Span { 152 | /// Given a `Span`, `fmt` will print out where the 153 | /// `Span` occurs in its source. Single-line 154 | /// `Span`s: 155 | /// ```ignore 156 | /// 12 | x = blatant { error } 157 | /// | ^^^^^^^^^^^^^^^^^ 158 | /// ``` 159 | /// Multi-line `Span`s: 160 | /// ```ignore 161 | /// 12 > x -> { 162 | /// 13 > y = x + 1 163 | /// 14 > another { error } 164 | /// 15 > } 165 | /// ``` 166 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 167 | write!(f, "{}", self.format()) 168 | } 169 | } 170 | 171 | /// Represents a formatted span, ready to be displayed. 172 | /// Contains information about where the span is from, 173 | /// and where in the text it starts and ends 174 | /// relative to the lines in the source. 175 | pub struct FormattedSpan { 176 | pub path: String, 177 | pub start: usize, 178 | pub lines: Vec, 179 | pub start_col: usize, 180 | pub end_col: usize, 181 | } 182 | 183 | impl FormattedSpan { 184 | pub fn is_multiline(&self) -> bool { 185 | self.lines.len() != 1 186 | } 187 | 188 | pub fn end(&self) -> usize { 189 | (self.start - 1) + self.lines.len() 190 | } 191 | 192 | pub fn gutter_padding(&self) -> usize { 193 | self.start.add(1).to_string().len() 194 | } 195 | 196 | /// If a single line span, returns the number of carrots 197 | /// between cols. 198 | pub fn carrots(&self) -> Option { 199 | if self.lines.len() == 1 { 200 | Some(self.end_col - self.start_col) 201 | } else { 202 | None 203 | } 204 | } 205 | } 206 | 207 | impl Display for FormattedSpan { 208 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 209 | writeln!( 210 | f, 211 | "In {}:{}:{}", 212 | self.path, 213 | self.start + 1, 214 | self.start_col + 1 215 | )?; 216 | writeln!(f, "{} |", " ".repeat(self.gutter_padding()))?; 217 | 218 | if !self.is_multiline() { 219 | writeln!(f, "{} | {}", self.start + 1, self.lines[0])?; 220 | writeln!( 221 | f, 222 | "{} | {}{}", 223 | " ".repeat(self.gutter_padding()), 224 | " ".repeat(self.start_col), 225 | "^".repeat(self.carrots().unwrap().max(1)), 226 | )?; 227 | } else { 228 | for (index, line) in self.lines.iter().enumerate() { 229 | let line_no = (self.start + index).to_string(); 230 | let padding = " ".repeat(self.gutter_padding() - line_no.len()); 231 | writeln!(f, "{}{} > {}", line_no, padding, line)?; 232 | } 233 | } 234 | 235 | Ok(()) 236 | } 237 | } 238 | 239 | /// A wrapper for spanning types. 240 | /// For example, a token, such as 241 | /// ``` 242 | /// pub enum Token { 243 | /// Number(f64), 244 | /// Open, 245 | /// Close, 246 | /// } 247 | /// ``` 248 | /// or the like, can be spanned to indicate where it was 249 | /// parsed from (a `Spanned`). 250 | #[derive(Clone, Eq, PartialEq)] 251 | pub struct Spanned { 252 | pub item: T, 253 | pub span: Span, 254 | } 255 | 256 | impl Spanned { 257 | /// Takes a generic item, and wraps in in a `Span` to 258 | /// make it `Spanned`. 259 | pub fn new(item: T, span: Span) -> Spanned { 260 | Spanned { item, span } 261 | } 262 | 263 | /// Joins a Vector of spanned items into a single span. 264 | pub fn build(spanneds: &[Spanned]) -> Option { 265 | let spans = spanneds 266 | .iter() 267 | .map(|s| s.span.clone()) 268 | .collect::>(); 269 | Span::join(spans) 270 | } 271 | 272 | /// Applies a function a `Spanned`'s item. 273 | pub fn try_map(self, f: fn(T) -> Result) -> Result, E> { 274 | Ok(Spanned::new(f(self.item)?, self.span)) 275 | } 276 | 277 | pub fn map(self, f: fn(T) -> B) -> Spanned { 278 | Spanned::new(f(self.item), self.span) 279 | } 280 | } 281 | 282 | impl Debug for Spanned { 283 | fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { 284 | Debug::fmt(&self.item, f)?; 285 | write!( 286 | f, 287 | " @ {}:{}", 288 | self.span.line(self.span.offset) + 1, 289 | self.span.col(self.span.offset) + 1, 290 | // self.span.offset, 291 | // self.span.end(), 292 | ) 293 | } 294 | } 295 | 296 | #[cfg(test)] 297 | mod test { 298 | use super::*; 299 | 300 | #[test] 301 | fn combination() { 302 | let source = Source::source("heck, that's awesome"); 303 | let a = Span::new(&source, 0, 5); 304 | let b = Span::new(&source, 11, 2); 305 | 306 | assert_eq!(Span::combine(&a, &b), Span::new(&source, 0, 13)); 307 | } 308 | 309 | #[test] 310 | fn span_and_contents() { 311 | let source = Source::source("hello, this is some text!"); 312 | let spans = vec![ 313 | Span::new(&source, 0, 8), 314 | Span::new(&source, 7, 5), 315 | Span::new(&source, 12, 4), 316 | ]; 317 | let result = Span::new(&source, 0, 16); 318 | 319 | assert_eq!(Span::join(spans).unwrap().contents(), result.contents()); 320 | } 321 | 322 | #[test] 323 | fn empty() { 324 | let source = Source::source(""); 325 | let span = Span::point(&source, 0); 326 | format!("{}", span); 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /passerine/src/vm/tag.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | f64, 3 | fmt::{Debug, Error, Formatter}, 4 | mem, 5 | }; 6 | 7 | use crate::{common::Data, vm::slot::Slot}; 8 | 9 | // TODO: add fallback for 32-bit systems and so on. 10 | /// `Tagged` implements Nan-tagging around the `Data` enum. 11 | /// In essence, it's possible to exploit the representation of `f64` NaNs 12 | /// to store pointers to other datatypes. 13 | /// 14 | /// When laid out, this is what the bit-level representation of a 15 | /// double-precision floating-point number looks like: 16 | /// ```plain 17 | /// SExponent---QIMantissa------------------------------------------ 18 | /// PNaN--------11D-Payload-------------------------------------...T 19 | /// ``` 20 | /// Where `S` is sign, `Q` is quiet flag, `I` is Intel’s "QNan Floating-Point 21 | /// Indefinite"; `P` is pointer flag, `D` is Data Tag (should always be 1), `T` 22 | /// is Tag. 23 | /// 24 | /// By exploiting this fact, assuming a 64-bit system, 25 | /// each item on the stack only takes up a machine word. 26 | /// This differs from having a stack of `Box`'d `Data`, 27 | /// because small items, like booleans, stack frames, etc. 28 | /// can be encoded directly into the tag 29 | /// rather than having to follow a pointer. 30 | /// It also keeps math fast for f64s, as a simple check and transmutation 31 | /// is all that's needed to reinterpret the bits as a valid number. 32 | /// 33 | /// > NOTE: implementation modeled after: 34 | /// > 35 | /// > - [rpjohnst/dejavu](https://github.com/rpjohnst/dejavu/blob/master/gml/src/vm/value.rs), 36 | /// > - and the Optimization chapter from Crafting Interpreters. 37 | /// > 38 | /// > Thank you! 39 | pub struct Tagged(u64); 40 | 41 | const QNAN: u64 = 0x7ffe_0000_0000_0000; 42 | const P_FLAG: u64 = 0x8000_0000_0000_0000; 43 | const P_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; 44 | const N_FLAG: u64 = 0x0000_0000_0000_0000; // not initialized 45 | const S_FLAG: u64 = 0x0000_0000_0000_0001; // stack frame 46 | const U_FLAG: u64 = 0x0000_0000_0000_0002; // unit 47 | const F_FLAG: u64 = 0x0000_0000_0000_0004; // false 48 | const T_FLAG: u64 = 0x0000_0000_0000_0008; // true 49 | 50 | impl Tagged { 51 | /// Wraps `Data` to create a new tagged pointer. 52 | pub fn new(slot: Slot) -> Tagged { 53 | match slot { 54 | // Float 55 | Slot::Data(Data::Float(f)) => Tagged(f.to_bits()), 56 | // Unit 57 | Slot::Data(Data::Unit) => Tagged(QNAN | U_FLAG), 58 | // True and false 59 | Slot::Data(Data::Boolean(false)) => Tagged(QNAN | F_FLAG), 60 | Slot::Data(Data::Boolean(true)) => Tagged(QNAN | T_FLAG), 61 | // Stack frame 62 | Slot::Frame => Tagged(QNAN | S_FLAG), 63 | // Not Initialized 64 | Slot::NotInit => Tagged(QNAN | N_FLAG), 65 | 66 | // on the heap 67 | // TODO: layout to make sure pointer is the right size when boxing 68 | other @ Slot::Data(_) | other @ Slot::Suspend { .. } | other @ Slot::Ref(_) => { 69 | Tagged(P_FLAG | QNAN | (P_MASK & (Box::into_raw(Box::new(other))) as u64)) 70 | } 71 | } 72 | } 73 | 74 | // TODO: encode frame in tag itself; a frame is not data 75 | /// Creates a new stack frame. 76 | #[inline] 77 | pub fn frame() -> Tagged { 78 | Tagged::new(Slot::Frame) 79 | } 80 | 81 | /// Shortcut for creating a new `Tagged(Slot::NotInit)`. 82 | #[inline] 83 | pub fn not_init() -> Tagged { 84 | Tagged::new(Slot::NotInit) 85 | } 86 | 87 | /// Returns the underlying `Data` (or a pointer to that `Data`). 88 | /// Unpacks the encoding used by [`Tagged`]. 89 | /// 90 | /// `dereference` will be called with a valid raw pointer to a [`Box`] 91 | /// 92 | /// The aliasing requirements on the source `Tagged` must be followed on 93 | /// this pointer. 94 | /// 95 | /// If the caller moves out of this pointer, the original [`Tagged`] must 96 | /// not be dropped 97 | fn extract(&self, dereference: impl FnOnce(*mut Slot) -> Slot) -> Slot { 98 | match self.0 { 99 | n if (n & QNAN) != QNAN => Slot::Data(Data::Float(f64::from_bits(n))), 100 | u if u == (QNAN | U_FLAG) => Slot::Data(Data::Unit), 101 | f if f == (QNAN | F_FLAG) => Slot::Data(Data::Boolean(false)), 102 | t if t == (QNAN | T_FLAG) => Slot::Data(Data::Boolean(true)), 103 | s if s == (QNAN | S_FLAG) => Slot::Frame, 104 | n if n == (QNAN | N_FLAG) => Slot::NotInit, 105 | p if (p & P_FLAG) == P_FLAG => dereference((p & P_MASK) as *mut Slot), 106 | _ => unreachable!("Corrupted tagged data"), 107 | } 108 | } 109 | 110 | /// Unwraps a tagged number into the appropriate datatype, 111 | /// consuming the tagged number. 112 | pub fn slot(self) -> Slot { 113 | let this = mem::ManuallyDrop::new(self); 114 | this.extract(|p| *unsafe { 115 | // Safety: We own `self` and the `Tagged` has been forgotten 116 | Box::from_raw(p) 117 | }) 118 | } 119 | 120 | /// Deeply copies some `Tagged` data. 121 | pub fn copy(&self) -> Slot { 122 | self.extract(|p| { 123 | unsafe { 124 | // Safety: We have a shared borrow of `self` 125 | &*p 126 | } 127 | .clone() 128 | }) 129 | } 130 | } 131 | 132 | impl Drop for Tagged { 133 | fn drop(&mut self) { 134 | self.extract(|p| *unsafe { 135 | // Safety: self will not be used again, so the contents can be 136 | // consumed 137 | Box::from_raw(p) 138 | }); 139 | } 140 | } 141 | 142 | impl Debug for Tagged { 143 | fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { 144 | write!(f, "Tagged({:?})", self.copy()) 145 | } 146 | } 147 | 148 | impl From for u64 { 149 | /// Unwraps a tagged pointer into the literal representation for debugging. 150 | fn from(tagged: Tagged) -> Self { 151 | tagged.0 152 | } 153 | } 154 | 155 | #[cfg(test)] 156 | mod test { 157 | use super::*; 158 | 159 | #[test] 160 | fn floats_eq() { 161 | let positive = 478_329.0; 162 | let negative = -231.0; 163 | let nan = f64::NAN; 164 | let neg_inf = f64::NEG_INFINITY; 165 | 166 | for n in &[positive, negative, nan, neg_inf] { 167 | let data = Data::Float(*n); 168 | let wrapped = Tagged::new(Slot::Data(data)); 169 | match wrapped.copy().data() { 170 | Data::Float(f) if f.is_nan() => assert!(n.is_nan()), 171 | Data::Float(f) => assert_eq!(*n, f), 172 | _ => panic!("Didn't unwrap to a real"), 173 | } 174 | } 175 | } 176 | 177 | #[test] 178 | fn bool_and_back() { 179 | assert_eq!( 180 | Data::Boolean(true), 181 | Tagged::new(Slot::Data(Data::Boolean(true))).copy().data() 182 | ); 183 | assert_eq!( 184 | Data::Boolean(false), 185 | Tagged::new(Slot::Data(Data::Boolean(false))).copy().data() 186 | ); 187 | } 188 | 189 | #[test] 190 | fn unit() { 191 | assert_eq!( 192 | Data::Unit, 193 | Tagged::new(Slot::Data(Data::Unit)).copy().data() 194 | ); 195 | } 196 | 197 | #[test] 198 | fn size() { 199 | let data_size = mem::size_of::(); 200 | let tag_size = mem::size_of::(); 201 | 202 | println!("Data size: {} bytes", data_size); 203 | println!("Tagged size: {} bytes", tag_size); 204 | 205 | // Tag == u64 == f64 == 64 206 | // If the tag is larger than the data, we're doing something wrong 207 | assert_eq!(tag_size, mem::size_of::()); 208 | assert!(tag_size < data_size); 209 | } 210 | 211 | #[test] 212 | fn string_pointer() { 213 | let s = "I just lost the game".to_string(); 214 | let three = "Elongated Muskrat".to_string(); 215 | let x = "It's kind of a dead giveaway, isn't it?".to_string(); 216 | 217 | for item in &[s, three, x] { 218 | let data = Data::String(item.clone()); 219 | let wrapped = Tagged::new(Slot::Data(data)); 220 | // println!("{:#b}", u64::from(wrapped)); 221 | match wrapped.copy().data() { 222 | Data::String(s) => { 223 | assert_eq!(item, &s) 224 | } 225 | _ => { 226 | // println!("{:#b}", u64::from(wrapped)); 227 | panic!("Didn't unwrap to a string"); 228 | } 229 | } 230 | } 231 | } 232 | 233 | #[test] 234 | fn other_tests_eq() { 235 | let tests = vec![ 236 | Data::Float(f64::consts::PI), 237 | Data::Float(-2.12), 238 | Data::Float(2.5E10), 239 | Data::Float(2.5e10), 240 | Data::Float(2.5E-10), 241 | Data::Float(0.5), 242 | Data::Float(f64::MAX), 243 | Data::Float(f64::MIN), 244 | Data::Float(f64::INFINITY), 245 | Data::Float(f64::NEG_INFINITY), 246 | Data::Float(f64::NAN), 247 | Data::Boolean(true), 248 | Data::Boolean(false), 249 | Data::Unit, 250 | Data::String("Hello, World!".to_string()), 251 | Data::String("".to_string()), 252 | Data::String("Whoop 😋".to_string()), 253 | ]; 254 | 255 | for test in tests { 256 | // println!("test: {:?}", test); 257 | let tagged = Tagged::new(Slot::Data(test.clone())); 258 | // println!("tagged: {:?}", tagged); 259 | let untagged = tagged.copy().data(); 260 | // println!("untagged: {:?}", untagged); 261 | // println!("---"); 262 | 263 | if let Data::Float(f) = untagged { 264 | if let Data::Float(n) = test { 265 | if n.is_nan() { 266 | assert!(f.is_nan()) 267 | } else { 268 | assert_eq!(test, Data::Float(n)); 269 | } 270 | } 271 | } else { 272 | assert_eq!(test, untagged); 273 | } 274 | } 275 | } 276 | 277 | #[test] 278 | fn no_leak_round() { 279 | // TODO: check memory was freed properly 280 | let location = "This is a string".to_string(); 281 | 282 | // drop dereferenced data 283 | let tagged = Tagged::new(Slot::Data(Data::String(location))); 284 | let pointer = tagged.0 & P_MASK; 285 | let untagged = tagged.copy().data(); 286 | // println!("-- Casting..."); 287 | let data = unsafe { Box::from_raw(pointer as *mut Data) }; 288 | // println!("before drop: {:?}", data); 289 | mem::forget(data); 290 | mem::drop(untagged); 291 | // println!("after drop: {:?}", data); 292 | } 293 | 294 | #[test] 295 | fn no_leak_tagged() { 296 | let location = "This is a string".to_string(); 297 | 298 | // drop tagged data 299 | let tagged = Tagged::new(Slot::Data(Data::String(location))); 300 | let pointer = tagged.0 & P_MASK; 301 | let data = unsafe { Box::from_raw(pointer as *mut Data) }; 302 | // println!("-- Dropping..."); 303 | // println!("before drop: {:?}", data); 304 | mem::forget(data); 305 | mem::drop(tagged); 306 | // println!("after drop: {:?}", data); 307 | } 308 | } 309 | --------------------------------------------------------------------------------