diff --git a/compiler/can/src/expr.rs b/compiler/can/src/expr.rs index 5b8edd794c..2cb7936451 100644 --- a/compiler/can/src/expr.rs +++ b/compiler/can/src/expr.rs @@ -169,9 +169,9 @@ pub struct Field { #[derive(Clone, Debug, PartialEq)] pub enum Recursive { - Recursive, - TailRecursive, - NotRecursive, + NotRecursive = 0, + Recursive = 1, + TailRecursive = 2, } #[derive(Clone, Debug, PartialEq)] diff --git a/editor/src/ast.rs b/editor/src/ast.rs index 969d30fc53..834765e907 100644 --- a/editor/src/ast.rs +++ b/editor/src/ast.rs @@ -243,75 +243,209 @@ pub enum IntStyle { Binary, } -/// Experimental idea for an Expr that fits in 16B +/// Experimental idea for an Expr that fits in 16B. +/// It has a 1B discriminant and variants which hold payloads of at most 15B. #[derive(Debug, Clone, PartialEq)] pub enum Expr2 { - /// An integer literal (without a dot) + /// A number literal (without a dot) containing no underscores Num { - number: i64, - var: Variable, - style: IntStyle, + number: i64, // 8B + var: Variable, // 4B + style: IntStyle, // 1B }, - /// A floating-point literal (with a dot) + /// A floating-point literal (with a dot) containing no underscores Float { - number: f64, - var: Variable, + number: f64, // 8B + var: Variable, // 4B }, - /// A formatted integer literal (containing underscores) - FormattedInt { - text_bytes: *const u8, - text_len: u8, // numeric literals can be at most 255 chars long - var: Variable, - style: IntStyle, + /// A number literal (without a dot) containing underscores + NumWithUnderscores { + number: i64, // 8B + var: Variable, // 4B + text: NodeId, // 3B }, - /// A formatted float literal (containing underscores) - FormattedFloat { - text_bytes: *const u8, - text_len: u8, // numeric literals can be at most 255 chars long - var: Variable, + /// A float literal (with a dot) containing underscores + FloatWithUnderscores { + number: f64, // 8B + var: Variable, // 4B + text: NodeId, // 3B }, - SmallStr(ArrayString), + /// string literals of length up to 14B + SmallStr(ArrayString), // 15B + /// string literals of length up to 4094B + MedStr(NodeId), // 4B + /// string literals of length over 4094B, but requires calling malloc/free BigStr { - bytes: *const u8, - len: u32, // string literals can be at most 2^32 (~4 billion) bytes long + pointer: *const u8, // 8B on 64-bit systems + len: u32, // 4B, meaning maximum string literal size of 4GB. Could theoretically fit 7B here, which would go up to the full isize::MAX }, + + // Lookups + Var(Symbol), // 8B + + /// Separate from List because BuckeList must be non-empty, and in this case + /// the list literal has no elements + EmptyList { + list_var: Variable, // 4B - required for uniqueness of the list + elem_var: Variable, // 4B + }, + + List { + list_var: Variable, // 4B - required for uniqueness of the list + elem_var: Variable, // 4B + elems: BucketList, // 4B + }, + If { - cond_var: Variable, - expr_var: Variable, - // Each branch is an (Expr, Expr) tuple. - // Make sure to put them in the bucket contiguously. - first_branch: ExprId, - num_branches: u8, - final_else: ExprId, + cond_var: Variable, // 4B + expr_var: Variable, // 4B + branches: BucketList<(Expr2, Expr2)>, // 4B + final_else: NodeId, // 3B }, When { - cond_var: Variable, - expr_var: Variable, - cond: ExprId, - // Make sure to put these branches - // in the bucket contiguously. - first_branch: WhenBranchId, - num_branches: u8, + cond_var: Variable, // 4B + expr_var: Variable, // 4B + branches: BucketList, // 4B + cond: ExprId, // 3B }, + LetRec { + // TODO need to make this Alias type here bucket-friendly, which will be hard! + aliases: BucketList<(Symbol, Alias)>, // 4B + defs: BucketList, // 4B + body_var: Variable, // 4B + body: NodeId, // 3B + }, + LetNonRec { + // TODO need to make this Alias type here bucket-friendly, which will be hard! + aliases: BucketList<(Symbol, Alias)>, // 4B + def: NodeId, // 3B + body: NodeId, // 3B + body_var: Variable, // 4B + }, + Call { + /// NOTE: the first elem in this list is the expression and its variable. + /// The others are arguments. This is because we didn't have room for + /// both the expr and its variable otherwise. + expr_and_args: BucketList<(Variable, NodeId)>, // 4B + fn_var: Variable, // 4B + closure_var: Variable, // 4B + called_via: CalledVia, // 1B + /// Cached outside expr_and_args so we don't have to potentially + /// traverse that whole linked list chain to count all the args. + arity: u16, // 2B + }, + RunLowLevel { + op: LowLevel, // 1B + args: BucketList<(Variable, NodeId)>, // 4B + ret_var: Variable, // 4B + }, + Closure { + /// NOTE: the first elem in this list is the function's name Symbol, plus Variable::NONE + /// + /// This is not ideal, but there's no room for an 8-byte Symbol + /// in a 16B node that already needs to hold this much other data. + captured_symbols: BucketList<(Symbol, Variable)>, // 4B + args: BucketList<(Variable, NodeId)>, // 4B + body: NodeId, // 3B + recursive: Recursive, // 1B + vars: NodeId, // 3B + }, + + // Product Types + Record { + record_var: Variable, // 4B + fields: BucketList<(BucketStr, Variable, NodeId)>, // 4B + }, + + /// Empty record constant + EmptyRecord, + + /// Look up exactly one field on a record, e.g. (expr).foo. + Access { + field: NodeId, // 3B + expr: NodeId, // 3B + vars: NodeId, // 3B + }, + + /// field accessor as a function, e.g. (.foo) expr + Accessor { + record_vars: NodeId, // 3B + function_var: Variable, // 4B + closure_var: Variable, // 4B + field: NodeId, // 3B + }, + Update { + symbol: Symbol, // 8B + updates: BucketList<(Lowercase, Field)>, // 4B + vars: NodeId, // 3B + }, + + // Sum Types + Tag { + // NOTE: A BucketStr node is a 2B length and then 14B bytes, + // plus more bytes in adjacent nodes if necessary. Thus we have + // a hard cap of 4094 bytes as the maximum length of tags and fields. + name: NodeId, // 3B + variant_var: Variable, // 4B + ext_var: Variable, // 4B + arguments: BucketList<(Variable, NodeId)>, // 4B + }, + + // Compiles, but will crash if reached + RuntimeError(RuntimeError), +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Def { + pub pattern: NodeId, // 3B + pub expr: NodeId, // 3B + // TODO maybe need to combine these vars behind a pointer? + pub expr_var: Variable, // 4B + pub pattern_vars: BucketList<(Symbol, Variable)>, // 4B + // TODO how big is an annotation? What about an Option? + pub annotation: Option, // ??? } #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct WhenBranchId { - /// TODO: WhenBranchBucketId - bucket_id: ExprBucketId, - /// TODO: WhenBranchBucketSlot - slot: ExprBucketSlot, +enum Pat2 { + Todo, +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct UpdateVars { + record_var: Variable, // 4B + ext_var: Variable, // 4B +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct RecordVars { + record_var: Variable, // 4B + ext_var: Variable, // 4B + field_var: Variable, // 4B +} + +/// This is 15B, so it fits in a Node slot. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct AccessVars { + record_var: Variable, // 4B + ext_var: Variable, // 4B + field_var: Variable, // 4B +} + +/// This is 16B, so it fits in a Node slot. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +struct ClosureVars { + function_type: Variable, + closure_type: Variable, + closure_ext_var: Variable, + return_type: Variable, } #[derive(Clone, Debug, PartialEq)] pub struct WhenBranch { - /// TODO: what if each branch had exactly 1 pattern? - /// That would save us 1B from storing the length. - pub first_pattern: PatternId, - pub num_patterns: u8, - pub body: ExprId, - /// TODO: should we have an ExprId::NULL for this? - pub guard: Option, + pub patterns: BucketList, // 4B + pub body: NodeId, // 3B + pub guard: Option>, // 4B } #[derive(Debug, Copy, Clone, PartialEq, Eq)] @@ -397,6 +531,12 @@ fn size_of_expr_bucket() { assert_eq!(std::mem::size_of::(), 4096); } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct PatId { + bucket_id: ExprBucketId, // TODO PatBucketId + slot: ExprBucketSlot, // TODO PatBucketSlot +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct ExprId { bucket_id: ExprBucketId, diff --git a/editor/src/bucket.rs b/editor/src/bucket.rs index 0bacde5df9..d5b399b36f 100644 --- a/editor/src/bucket.rs +++ b/editor/src/bucket.rs @@ -1,23 +1,40 @@ -/// A bucket +/// A bucket of 16-byte nodes. The node value 0 is reserved for the bucket's +/// use, and valid nodes may never have that value. +/// +/// By design, each bucket is 4096 bytes large. When you make a bucket, it +/// uses mmap to reserve one anonymous memory page in which to store nodes. +/// Since nodes are 16 bytes, one bucket can store 256 nodes; you can access +/// a particular node by its BucketSlot, which is an opaque wrapper around a u8. +/// +/// Buckets also use the node value 0 (all 0 bits) to mark slots as unoccupied. +/// This is important for performance. use libc::{c_void, calloc, free, mmap, munmap, MAP_ANONYMOUS, MAP_PRIVATE, PROT_READ, PROT_WRITE}; use std::marker::PhantomData; use std::mem::{self, size_of}; use std::ptr::null; -use std::{u16, u8}; const BUCKET_BYTES: usize = 4096; -pub struct NodeId { +pub struct NodeId { pub bucket_id: BucketId, pub slot: BucketSlot, } -pub struct BucketId { +impl NodeId { + fn next_slot(&self) -> Self { + NodeId { + bucket_id: self.bucket_id, + slot: self.slot.increment(), + } + } +} + +pub struct BucketId { value: u16, _phantom: PhantomData, } -impl BucketId { +impl BucketId { fn from_u16(value: u16) -> Self { BucketId { value, @@ -26,26 +43,36 @@ impl BucketId { } } -pub struct BucketSlot { +pub struct BucketSlot { value: u8, _phantom: PhantomData, } -impl BucketSlot { +impl BucketSlot { fn from_u8(value: u8) -> Self { BucketSlot { value, _phantom: PhantomData::default(), } } + + fn increment(&self) -> Self { + BucketSlot { + value: self.value + 1, + _phantom: PhantomData::default(), + } + } } -pub struct Buckets { - buckets: Vec>, +pub struct Buckets { + buckets: Vec, + // free_1node_slots: Vec>, } -impl Buckets { - pub fn add(&mut self) -> Result, ()> { +impl Buckets { + // fn find_space_for(&mut self, nodes: u8) -> Result, ()> {} + + pub fn add(&mut self, node: T) -> Result, ()> { let num_buckets = self.buckets.len(); if num_buckets <= u16::MAX as usize { @@ -59,23 +86,33 @@ impl Buckets { } } - pub fn get<'a>(&'a self, node_id: NodeId) -> Option<&'a T> { + fn get_unchecked<'a, T: Sized>(&'a self, node_id: NodeId) -> &'a T { + self.buckets + .get(node_id.bucket_id.value as usize) + .unwrap() + .get_unchecked(node_id.slot.value) + } + + pub fn get<'a, T: Sized>(&'a self, node_id: NodeId) -> Option<&'a T> { self.buckets .get(node_id.bucket_id.value as usize) .and_then(|bucket| bucket.get(node_id.slot)) } } -pub struct Bucket { +struct Bucket { next_unused_slot: u16, - first_slot: *mut T, - _phantom: PhantomData, + first_slot: *mut [u8; 16], } -impl Bucket { +impl Bucket { /// If there's room left in the bucket, adds the item and returns /// the slot where it was put. If there was no room left, returns Err(()). - pub fn add(&mut self, node: T) -> Result, ()> { + pub fn add(&mut self, node: T) -> Result, ()> { + // It's only safe to store this as a *const T if T is 16 bytes. + // This is designed to be used exclusively with 16-byte nodes! + debug_assert_eq!(size_of::(), 16); + // Once next_unused_slot exceeds u8::MAX, we have no room left. if self.next_unused_slot <= u8::MAX as u16 { let chosen_slot = self.next_unused_slot as u8; @@ -92,7 +129,11 @@ impl Bucket { /// If the given slot is available, inserts the given node into it. /// Otherwise, returns the node that was in the already-occupied slot. - pub fn insert(&mut self, node: T, slot: BucketSlot) -> Result<(), &T> { + pub fn insert(&mut self, node: T, slot: BucketSlot) -> Result<(), &T> { + // It's only safe to use this if T is 16 bytes. + // This is designed to be used exclusively with 16-byte nodes! + debug_assert_eq!(size_of::(), 16); + let slot = slot.value; unsafe { @@ -106,7 +147,11 @@ impl Bucket { } } - pub fn get<'a>(&'a self, slot: BucketSlot) -> Option<&'a T> { + pub fn get<'a, T: Sized>(&'a self, slot: BucketSlot) -> Option<&'a T> { + // It's only safe to store this as a *const T if T is 16 bytes. + // This is designed to be used exclusively with 16-byte nodes! + debug_assert_eq!(size_of::(), 16); + unsafe { let slot_ptr = self.first_slot.offset(slot.value as isize) as *const T; let value = &*slot_ptr; @@ -119,13 +164,17 @@ impl Bucket { } } - unsafe fn put_unchecked(&mut self, node: T, slot: u8) { + unsafe fn put_unchecked(&mut self, node: T, slot: u8) { + // It's only safe to store this as a *const T if T is 16 bytes. + // This is designed to be used exclusively with 16-byte nodes! + debug_assert_eq!(size_of::(), 16); + let slot_ptr = self.first_slot.offset(slot as isize); *slot_ptr = node; } - unsafe fn get_unchecked<'a>(&'a self, slot: u8) -> &'a T { + unsafe fn get_unchecked<'a, T>(&'a self, slot: u8) -> &'a T { &*self.first_slot.offset(slot as isize) } @@ -137,12 +186,8 @@ impl Bucket { } } -impl Default for Bucket { +impl Default for Bucket { fn default() -> Self { - // It's only safe to store this as a *const T if T is 16 bytes. - // This is designed to be used exclusively with 16-byte nodes! - debug_assert_eq!(size_of::(), 16); - let first_slot = if page_size::get() == 4096 { unsafe { // mmap exactly one memory page (4096 bytes) @@ -160,7 +205,7 @@ impl Default for Bucket { // (We use calloc over malloc because we rely on the bytes having // been zeroed to tell which slots are available.) unsafe { calloc(1, BUCKET_BYTES) } - } as *mut T; + } as *mut [u8; 16]; Bucket { next_unused_slot: 0, @@ -170,7 +215,7 @@ impl Default for Bucket { } } -impl Drop for Bucket { +impl Drop for Bucket { fn drop(&mut self) { if page_size::get() == 4096 { unsafe { @@ -183,3 +228,118 @@ impl Drop for Bucket { } } } + +/// A non-empty list inside a bucket. It takes 4B of memory. +/// +/// This is internally represented as an array of at most 255 nodes, which +/// can grow to 256+ nodes by having the last nodeent be a linked list Cons +/// cell which points to another such backing array which has more nodes. +/// +/// In practice, these will almost be far below 256 nodes, but in theory +/// they can be enormous in length thanks to the linked list fallback. +/// +/// Since these are non-empty lists, we need separate variants for collections +/// that can be empty, e.g. EmptyRecord and EmptyList. In contrast, we don't +/// need an EmptyList or EmptyWhen, since although those use BucketList +/// to store their branches, having zero branches is syntactically invalid. +/// Same with Call and Closure, since all functions must have 1+ arguments. +pub struct BucketList { + first_node_id: NodeId, + first_segment_len: u8, +} + +impl BucketList { + /// If given a first_segment_len of 0, that means this is a BucketList + /// consisting of 256+ nodes. The first 255 are stored in the usual + /// array, and then there's one more nodeent at the end which continues + /// the list with a new length and NodeId value. BucketList iterators + /// automatically do these jumps behind the scenes when necessary. + pub fn new(first_node_id: NodeId, first_segment_len: u8) -> Self { + BucketList { + first_segment_len, + first_node_id, + } + } + + pub fn into_iter<'a>(self, buckets: &'a Buckets) -> impl Iterator { + self.into_bucket_list_iter(buckets) + } + + /// Private version of into_iter which exposes the implementation detail + /// of BucketListIter. We don't want that struct to be public, but we + /// actually do want to have this separate function for code reuse + /// in the iterator's next() method. + fn into_bucket_list_iter<'a>(self, buckets: &'a Buckets) -> BucketListIter<'a, T> { + let first_segment_len = self.first_segment_len; + let continues_with_cons = first_segment_len == 0; + let len_remaining = if continues_with_cons { + // We have 255 nodes followed by a Cons cell continuing the list. + u8::MAX + } else { + first_segment_len + }; + + BucketListIter { + continues_with_cons, + len_remaining, + node_id: self.first_node_id, + buckets, + } + } +} + +struct BucketListIter<'a, T: Sized> { + node_id: NodeId, + len_remaining: u8, + continues_with_cons: bool, + buckets: &'a Buckets, +} + +impl<'a, T: Sized> Iterator for BucketListIter<'a, T> { + type Item = &'a T; + + fn next(&mut self) -> Option { + match self.len_remaining { + 0 => match self.continues_with_cons { + // We're done! This is by far the most common case, so we put + // it first to avoid branch mispredictions. + False => None, + // We need to continue with a Cons cell. + True => { + // Since we have continues_with_cons set, the next slot + // will definitely be occupied with a BucketList struct. + let node = self.buckets.get_unchecked(self.node_id.next_slot()); + let next_list = unsafe { mem::transmute::<&T, &BucketList>(node) }; + + // Replace the current iterator with an iterator into that + // list, and then continue with next() on that iterator. + let next_iter = next_list.into_bucket_list_iter(self.buckets); + + self.node_id = next_iter.node_id; + self.len_remaining = next_iter.len_remaining; + self.continues_with_cons = next_iter.continues_with_cons; + + self.next() + } + }, + 1 => { + self.len_remaining = 0; + + // Don't advance the node pointer's slot, because that might + // advance past the end of the bucket! + + Some(self.buckets.get_unchecked(self.node_id)) + } + len_remaining => { + // Get the current node + let node = self.buckets.get_unchecked(self.node_id); + + // Advance the node pointer to the next slot in the current bucket + self.node_id = self.node_id.next_slot(); + self.len_remaining = len_remaining - 1; + + Some(node) + } + } + } +}