diff --git a/CHANGELOG.md b/CHANGELOG.md index 618262432..d0ce32560 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## 0.11.0 (TBD) +#### Enhancements + +- Added support for procedure annotation (attribute) syntax to Miden Assembly + #### Changes - [BREAKING] Wrapped `MastForest`s in `Program` and `Library` structs in `Arc` (#1465). diff --git a/assembly/src/ast/attribute/meta.rs b/assembly/src/ast/attribute/meta.rs new file mode 100644 index 000000000..29973cf75 --- /dev/null +++ b/assembly/src/ast/attribute/meta.rs @@ -0,0 +1,253 @@ +mod expr; +mod kv; +mod list; + +use alloc::{collections::BTreeMap, string::String, sync::Arc, vec::Vec}; +use core::fmt; + +pub use self::{expr::MetaExpr, kv::MetaKeyValue, list::MetaList}; +use crate::{ast::Ident, parser::HexEncodedValue, Felt, SourceSpan, Span}; + +/// Represents the metadata provided as arguments to an attribute. +#[derive(Clone, PartialEq, Eq)] +pub enum Meta { + /// Represents empty metadata, e.g. `@foo` + Unit, + /// A list of metadata expressions, e.g. `@foo(a, "some text", 0x01)` + /// + /// The list should always have at least one element, and this is guaranteed by the parser. + List(Vec), + /// A set of uniquely-named metadata expressions, e.g. `@foo(letter = a, text = "some text")` + /// + /// The set should always have at least one key-value pair, and this is guaranteed by the + /// parser. + KeyValue(BTreeMap), +} +impl Meta { + /// Borrow the metadata without unwrapping the specific type + /// + /// Returns `None` if there is no meaningful metadata + #[inline] + pub fn borrow(&self) -> Option> { + match self { + Self::Unit => None, + Self::List(ref list) => Some(BorrowedMeta::List(list)), + Self::KeyValue(ref kv) => Some(BorrowedMeta::KeyValue(kv)), + } + } +} +impl FromIterator for Meta { + #[inline] + fn from_iter>(iter: T) -> Self { + let mut iter = iter.into_iter(); + match iter.next() { + None => Self::Unit, + Some(MetaItem::Expr(expr)) => Self::List( + core::iter::once(expr) + .chain(iter.map(|item| match item { + MetaItem::Expr(expr) => expr, + MetaItem::KeyValue(..) => unsafe { core::hint::unreachable_unchecked() }, + })) + .collect(), + ), + Some(MetaItem::KeyValue(k, v)) => Self::KeyValue( + core::iter::once((k, v)) + .chain(iter.map(|item| match item { + MetaItem::KeyValue(k, v) => (k, v), + MetaItem::Expr(_) => unsafe { core::hint::unreachable_unchecked() }, + })) + .collect(), + ), + } + } +} + +impl FromIterator for Meta { + #[inline] + fn from_iter>(iter: T) -> Self { + Self::List(iter.into_iter().collect()) + } +} + +impl FromIterator<(Ident, MetaExpr)> for Meta { + #[inline] + fn from_iter>(iter: T) -> Self { + Self::KeyValue(iter.into_iter().collect()) + } +} + +impl<'a> FromIterator<(&'a str, MetaExpr)> for Meta { + #[inline] + fn from_iter(iter: T) -> Self + where + T: IntoIterator, + { + Self::KeyValue( + iter.into_iter() + .map(|(k, v)| { + let k = Ident::new_unchecked(Span::new(SourceSpan::UNKNOWN, Arc::from(k))); + (k, v) + }) + .collect(), + ) + } +} + +impl From for Meta +where + Meta: FromIterator, + I: IntoIterator, +{ + #[inline] + fn from(iter: I) -> Self { + Self::from_iter(iter) + } +} + +/// Represents a reference to the metadata for an [super::Attribute] +/// +/// See [Meta] for what metadata is represented, and its syntax. +#[derive(Copy, Clone, PartialEq, Eq)] +pub enum BorrowedMeta<'a> { + /// A list of metadata expressions + List(&'a [MetaExpr]), + /// A list of uniquely-named metadata expressions + KeyValue(&'a BTreeMap), +} +impl fmt::Debug for BorrowedMeta<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::List(items) => write!(f, "{items:#?}"), + Self::KeyValue(items) => write!(f, "{items:#?}"), + } + } +} + +/// Represents a single metadata item provided as an argument to an attribute. +/// +/// For example, the `foo` attribute in `@foo(bar, baz)` has two metadata items, both of `Expr` +/// type, which compose a ` +#[derive(Clone, PartialEq, Eq)] +pub enum MetaItem { + /// A metadata expression, e.g. `"some text"` in `@foo("some text")` + /// + /// This represents the element type for `Meta::List`-based attributes. + Expr(MetaExpr), + /// A named metadata expression, e.g. `letter = a` in `@foo(letter = a)` + /// + /// This represents the element type for `Meta::KeyValue`-based attributes. + KeyValue(Ident, MetaExpr), +} + +impl MetaItem { + /// Unwrap this item to extract the contained [MetaExpr]. + /// + /// Panics if this item is not the `Expr` variant. + #[inline] + #[track_caller] + pub fn unwrap_expr(self) -> MetaExpr { + match self { + Self::Expr(expr) => expr, + Self::KeyValue(..) => unreachable!("tried to unwrap key-value as expression"), + } + } + + /// Unwrap this item to extract the contained key-value pair. + /// + /// Panics if this item is not the `KeyValue` variant. + #[inline] + #[track_caller] + pub fn unwrap_key_value(self) -> (Ident, MetaExpr) { + match self { + Self::KeyValue(k, v) => (k, v), + Self::Expr(_) => unreachable!("tried to unwrap expression as key-value"), + } + } +} + +impl From for MetaItem { + fn from(value: Ident) -> Self { + Self::Expr(MetaExpr::Ident(value)) + } +} + +impl From<&str> for MetaItem { + fn from(value: &str) -> Self { + Self::Expr(MetaExpr::String(Ident::new_unchecked(Span::new( + SourceSpan::UNKNOWN, + Arc::from(value), + )))) + } +} + +impl From for MetaItem { + fn from(value: String) -> Self { + Self::Expr(MetaExpr::String(Ident::new_unchecked(Span::new( + SourceSpan::UNKNOWN, + Arc::from(value.into_boxed_str()), + )))) + } +} + +impl From for MetaItem { + fn from(value: u8) -> Self { + Self::Expr(MetaExpr::Int(Span::new(SourceSpan::UNKNOWN, HexEncodedValue::U8(value)))) + } +} + +impl From for MetaItem { + fn from(value: u16) -> Self { + Self::Expr(MetaExpr::Int(Span::new(SourceSpan::UNKNOWN, HexEncodedValue::U16(value)))) + } +} + +impl From for MetaItem { + fn from(value: u32) -> Self { + Self::Expr(MetaExpr::Int(Span::new(SourceSpan::UNKNOWN, HexEncodedValue::U32(value)))) + } +} + +impl From for MetaItem { + fn from(value: Felt) -> Self { + Self::Expr(MetaExpr::Int(Span::new(SourceSpan::UNKNOWN, HexEncodedValue::Felt(value)))) + } +} + +impl From<[Felt; 4]> for MetaItem { + fn from(value: [Felt; 4]) -> Self { + Self::Expr(MetaExpr::Int(Span::new(SourceSpan::UNKNOWN, HexEncodedValue::Word(value)))) + } +} + +impl From<(Ident, V)> for MetaItem +where + V: Into, +{ + fn from(entry: (Ident, V)) -> Self { + let (key, value) = entry; + Self::KeyValue(key, value.into()) + } +} + +impl From<(&str, V)> for MetaItem +where + V: Into, +{ + fn from(entry: (&str, V)) -> Self { + let (key, value) = entry; + let key = Ident::new_unchecked(Span::new(SourceSpan::UNKNOWN, Arc::from(key))); + Self::KeyValue(key, value.into()) + } +} + +impl From<(String, V)> for MetaItem +where + V: Into, +{ + fn from(entry: (String, V)) -> Self { + let (key, value) = entry; + let key = + Ident::new_unchecked(Span::new(SourceSpan::UNKNOWN, Arc::from(key.into_boxed_str()))); + Self::KeyValue(key, value.into()) + } +} diff --git a/assembly/src/ast/attribute/meta/expr.rs b/assembly/src/ast/attribute/meta/expr.rs new file mode 100644 index 000000000..8c92899b2 --- /dev/null +++ b/assembly/src/ast/attribute/meta/expr.rs @@ -0,0 +1,86 @@ +use alloc::{string::String, sync::Arc}; + +use crate::{ast::Ident, parser::HexEncodedValue, prettier, Felt, SourceSpan, Span, Spanned}; + +/// Represents a metadata expression of an [crate::ast::Attribute] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum MetaExpr { + /// An identifier/keyword, e.g. `inline` + Ident(Ident), + /// A decimal or hexadecimal integer value + Int(Span), + /// A quoted string or identifier + String(Ident), +} + +impl prettier::PrettyPrint for MetaExpr { + fn render(&self) -> prettier::Document { + use prettier::*; + + match self { + Self::Ident(id) => text(id), + Self::Int(value) => text(value), + Self::String(id) => text(format!("\"{}\"", id.as_str().escape_default())), + } + } +} + +impl From for MetaExpr { + fn from(value: Ident) -> Self { + Self::Ident(value) + } +} + +impl From<&str> for MetaExpr { + fn from(value: &str) -> Self { + Self::String(Ident::new_unchecked(Span::new(SourceSpan::UNKNOWN, Arc::from(value)))) + } +} + +impl From for MetaExpr { + fn from(value: String) -> Self { + Self::String(Ident::new_unchecked(Span::new( + SourceSpan::UNKNOWN, + Arc::from(value.into_boxed_str()), + ))) + } +} + +impl From for MetaExpr { + fn from(value: u8) -> Self { + Self::Int(Span::new(SourceSpan::UNKNOWN, HexEncodedValue::U8(value))) + } +} + +impl From for MetaExpr { + fn from(value: u16) -> Self { + Self::Int(Span::new(SourceSpan::UNKNOWN, HexEncodedValue::U16(value))) + } +} + +impl From for MetaExpr { + fn from(value: u32) -> Self { + Self::Int(Span::new(SourceSpan::UNKNOWN, HexEncodedValue::U32(value))) + } +} + +impl From for MetaExpr { + fn from(value: Felt) -> Self { + Self::Int(Span::new(SourceSpan::UNKNOWN, HexEncodedValue::Felt(value))) + } +} + +impl From<[Felt; 4]> for MetaExpr { + fn from(value: [Felt; 4]) -> Self { + Self::Int(Span::new(SourceSpan::UNKNOWN, HexEncodedValue::Word(value))) + } +} + +impl Spanned for MetaExpr { + fn span(&self) -> SourceSpan { + match self { + Self::Ident(spanned) | Self::String(spanned) => spanned.span(), + Self::Int(spanned) => spanned.span(), + } + } +} diff --git a/assembly/src/ast/attribute/meta/kv.rs b/assembly/src/ast/attribute/meta/kv.rs new file mode 100644 index 000000000..69ed38936 --- /dev/null +++ b/assembly/src/ast/attribute/meta/kv.rs @@ -0,0 +1,135 @@ +use alloc::collections::BTreeMap; +use core::borrow::Borrow; + +use super::MetaExpr; +use crate::{ast::Ident, SourceSpan, Spanned}; + +/// Represents the metadata of a key-value [crate::ast::Attribute], i.e. `@props(key = value)` +#[derive(Clone)] +pub struct MetaKeyValue { + pub span: SourceSpan, + /// The name of the key-value dictionary + pub name: Ident, + /// The set of key-value pairs provided as arguments to this attribute + pub items: BTreeMap, +} + +impl Spanned for MetaKeyValue { + #[inline(always)] + fn span(&self) -> SourceSpan { + self.span + } +} + +impl MetaKeyValue { + pub fn new(name: Ident, items: I) -> Self + where + I: IntoIterator, + K: Into, + V: Into, + { + let items = items.into_iter().map(|(k, v)| (k.into(), v.into())).collect(); + Self { span: SourceSpan::default(), name, items } + } + + pub fn with_span(mut self, span: SourceSpan) -> Self { + self.span = span; + self + } + + /// Get the name of this metadata as a string + #[inline] + pub fn name(&self) -> &str { + self.name.as_str() + } + + /// Get the name of this metadata as an [Ident] + #[inline] + pub fn id(&self) -> Ident { + self.name.clone() + } + + /// Returns true if this metadata contains an entry for `key` + pub fn contains_key(&self, key: &Q) -> bool + where + Ident: Borrow + Ord, + Q: ?Sized + Ord, + { + self.items.contains_key(key) + } + + /// Returns the value associated with `key`, if present in this metadata + pub fn get(&self, key: &Q) -> Option<&MetaExpr> + where + Ident: Borrow + Ord, + Q: ?Sized + Ord, + { + self.items.get(key) + } + + /// Inserts a new key-value entry in this metadata + pub fn insert(&mut self, key: impl Into, value: impl Into) { + self.items.insert(key.into(), value.into()); + } + + /// Removes the entry associated with `key`, if present in this metadata, and returns it + pub fn remove(&mut self, key: &Q) -> Option + where + Ident: Borrow + Ord, + Q: ?Sized + Ord, + { + self.items.remove(key) + } + + /// Get an entry in the key-value map of this metadata for `key` + pub fn entry( + &mut self, + key: Ident, + ) -> alloc::collections::btree_map::Entry<'_, Ident, MetaExpr> { + self.items.entry(key) + } + + /// Get an iterator over the the key-value items of this metadata + #[inline] + pub fn iter(&self) -> impl Iterator { + self.items.iter() + } +} + +impl IntoIterator for MetaKeyValue { + type Item = (Ident, MetaExpr); + type IntoIter = alloc::collections::btree_map::IntoIter; + + #[inline] + fn into_iter(self) -> Self::IntoIter { + self.items.into_iter() + } +} + +impl Eq for MetaKeyValue {} + +impl PartialEq for MetaKeyValue { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.items == other.items + } +} + +impl PartialOrd for MetaKeyValue { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MetaKeyValue { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.name.cmp(&other.name).then_with(|| self.items.cmp(&other.items)) + } +} + +impl core::hash::Hash for MetaKeyValue { + fn hash(&self, state: &mut H) { + self.name.hash(state); + self.items.hash(state); + } +} diff --git a/assembly/src/ast/attribute/meta/list.rs b/assembly/src/ast/attribute/meta/list.rs new file mode 100644 index 000000000..99cb6e61e --- /dev/null +++ b/assembly/src/ast/attribute/meta/list.rs @@ -0,0 +1,102 @@ +use alloc::vec::Vec; + +use super::MetaExpr; +use crate::{ast::Ident, SourceSpan, Spanned}; + +/// Represents the metadata of a named list [crate::ast::Attribute], i.e. `@name(item0, .., itemN)` +#[derive(Clone)] +pub struct MetaList { + pub span: SourceSpan, + /// The identifier used as the name of this attribute + pub name: Ident, + /// The list of items representing the value of this attribute - will always contain at least + /// one element when parsed. + pub items: Vec, +} + +impl Spanned for MetaList { + #[inline(always)] + fn span(&self) -> SourceSpan { + self.span + } +} + +impl MetaList { + pub fn new(name: Ident, items: I) -> Self + where + I: IntoIterator, + { + Self { + span: SourceSpan::default(), + name, + items: items.into_iter().collect(), + } + } + + pub fn with_span(mut self, span: SourceSpan) -> Self { + self.span = span; + self + } + + /// Get the name of this attribute as a string + pub fn name(&self) -> &str { + self.name.as_str() + } + + /// Get the name of this attribute as an [Ident] + pub fn id(&self) -> Ident { + self.name.clone() + } + + /// Returns true if the metadata list is empty + #[inline] + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + /// Returns the number of items in the metadata list + #[inline] + pub fn len(&self) -> usize { + self.items.len() + } + + /// Get the metadata list as a slice + #[inline] + pub fn as_slice(&self) -> &[MetaExpr] { + self.items.as_slice() + } + + /// Get the metadata list as a mutable slice + #[inline] + pub fn as_mut_slice(&mut self) -> &mut [MetaExpr] { + self.items.as_mut_slice() + } +} + +impl Eq for MetaList {} + +impl PartialEq for MetaList { + fn eq(&self, other: &Self) -> bool { + self.name == other.name && self.items == other.items + } +} + +impl PartialOrd for MetaList { + #[inline] + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for MetaList { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + self.name.cmp(&other.name).then_with(|| self.items.cmp(&other.items)) + } +} + +impl core::hash::Hash for MetaList { + fn hash(&self, state: &mut H) { + self.name.hash(state); + self.items.hash(state); + } +} diff --git a/assembly/src/ast/attribute/mod.rs b/assembly/src/ast/attribute/mod.rs new file mode 100644 index 000000000..3c7937ba5 --- /dev/null +++ b/assembly/src/ast/attribute/mod.rs @@ -0,0 +1,246 @@ +mod meta; +mod set; + +use core::fmt; + +pub use self::{ + meta::{BorrowedMeta, Meta, MetaExpr, MetaItem, MetaKeyValue, MetaList}, + set::{AttributeSet, AttributeSetEntry}, +}; +use crate::{ast::Ident, prettier, SourceSpan, Spanned}; + +/// An [Attribute] represents some named metadata attached to a Miden Assembly procedure. +/// +/// An attribute has no predefined structure per se, but syntactically there are three types: +/// +/// * Marker attributes, i.e. just a name and no associated data. Attributes of this type are used +/// to "mark" the item they are attached to with some unique trait or behavior implied by the +/// name. For example, `@inline`. NOTE: `@inline()` is not valid syntax. +/// +/// * List attributes, i.e. a name and one or more comma-delimited expressions. Attributes of this +/// type are used for cases where you want to parameterize a marker-like trait. To use a Rust +/// example, `#[derive(Trait)]` is a list attribute, where `derive` is the marker, but we want to +/// instruct whatever processes derives, what traits it needs to derive. The equivalent syntax in +/// Miden Assembly would be `@derive(Trait)`. Lists must always have at least one item. +/// +/// * Key-value attributes, i.e. a name and a value. Attributes of this type are used to attach +/// named properties to an item. For example, `@storage(offset = 1)`. Possible value types are: +/// bare identifiers, decimal or hexadecimal integers, and quoted strings. +/// +/// There are no restrictions on what attributes can exist or be used. However, there are a set of +/// attributes that the assembler knows about, and acts on, which will be stripped during assembly. +/// Any remaining attributes we don't explicitly handle in the assembler, will be passed along as +/// metadata attached to the procedures in the MAST output by the assembler. +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum Attribute { + /// A named behavior, trait or action; e.g. `@inline` + Marker(Ident), + /// A parameterized behavior, trait or action; e.g. `@inline(always)` or `@derive(Foo, Bar)` + List(MetaList), + /// A named property; e.g. `@props(key = "value")`, `@props(a = 1, b = 0x1)` + KeyValue(MetaKeyValue), +} + +impl Attribute { + /// Create a new [Attribute] with the given metadata. + /// + /// The metadata value must be convertible to [Meta]. + /// + /// For marker attributes, you can either construct the `Marker` variant directly, or pass + /// either `Meta::Unit` or `None` as the metadata argument. + /// + /// If the metadata is empty, a `Marker` attribute will be produced, otherwise the type depends + /// on the metadata. If the metadata is _not_ key-value shaped, a `List` is produced, otherwise + /// a `KeyValue`. + pub fn new(name: Ident, metadata: impl Into) -> Self { + let metadata = metadata.into(); + match metadata { + Meta::Unit => Self::Marker(name), + Meta::List(items) => Self::List(MetaList { span: Default::default(), name, items }), + Meta::KeyValue(items) => { + Self::KeyValue(MetaKeyValue { span: Default::default(), name, items }) + }, + } + } + + /// Create a new [Attribute] from an metadata-producing iterator. + /// + /// If the iterator is empty, a `Marker` attribute will be produced, otherwise the type depends + /// on the metadata. If the metadata is _not_ key-value shaped, a `List` is produced, otherwise + /// a `KeyValue`. + pub fn from_iter(name: Ident, metadata: I) -> Self + where + Meta: FromIterator, + I: IntoIterator, + { + Self::new(name, Meta::from_iter(metadata)) + } + + /// Set the source location for this attribute + pub fn with_span(self, span: SourceSpan) -> Self { + match self { + Self::Marker(id) => Self::Marker(id.with_span(span)), + Self::List(list) => Self::List(list.with_span(span)), + Self::KeyValue(kv) => Self::KeyValue(kv.with_span(span)), + } + } + + /// Get the name of this attribute as a string + pub fn name(&self) -> &str { + match self { + Self::Marker(id) => id.as_str(), + Self::List(list) => list.name(), + Self::KeyValue(kv) => kv.name(), + } + } + + /// Get the name of this attribute as an [Ident] + pub fn id(&self) -> Ident { + match self { + Self::Marker(id) => id.clone(), + Self::List(list) => list.id(), + Self::KeyValue(kv) => kv.id(), + } + } + + /// Returns true if this is a marker attribute + pub fn is_marker(&self) -> bool { + matches!(self, Self::Marker(_)) + } + + /// Returns true if this is a list attribute + pub fn is_list(&self) -> bool { + matches!(self, Self::List(_)) + } + + /// Returns true if this is a key-value attribute + pub fn is_key_value(&self) -> bool { + matches!(self, Self::KeyValue(_)) + } + + /// Get the metadata for this attribute + /// + /// Returns `None` if this is a marker attribute, and thus has no metadata + pub fn metadata(&self) -> Option> { + match self { + Self::Marker(_) => None, + Self::List(ref list) => Some(BorrowedMeta::List(&list.items)), + Self::KeyValue(ref kv) => Some(BorrowedMeta::KeyValue(&kv.items)), + } + } +} + +impl fmt::Debug for Attribute { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Marker(id) => f.debug_tuple("Marker").field(&id).finish(), + Self::List(meta) => f + .debug_struct("List") + .field("name", &meta.name) + .field("items", &meta.items) + .finish(), + Self::KeyValue(meta) => f + .debug_struct("KeyValue") + .field("name", &meta.name) + .field("items", &meta.items) + .finish(), + } + } +} + +impl fmt::Display for Attribute { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use prettier::PrettyPrint; + self.pretty_print(f) + } +} + +impl prettier::PrettyPrint for Attribute { + fn render(&self) -> prettier::Document { + use prettier::*; + let doc = text(format!("@{}", &self.name())); + match self { + Self::Marker(_) => doc, + Self::List(meta) => { + let singleline_items = meta + .items + .iter() + .map(|item| item.render()) + .reduce(|acc, item| acc + const_text(", ") + item) + .unwrap_or(Document::Empty); + let multiline_items = indent( + 4, + nl() + meta + .items + .iter() + .map(|item| item.render()) + .reduce(|acc, item| acc + nl() + item) + .unwrap_or(Document::Empty), + ) + nl(); + doc + const_text("(") + (singleline_items | multiline_items) + const_text(")") + }, + Self::KeyValue(meta) => { + let singleline_items = meta + .items + .iter() + .map(|(k, v)| text(k) + const_text(" = ") + v.render()) + .reduce(|acc, item| acc + const_text(", ") + item) + .unwrap_or(Document::Empty); + let multiline_items = indent( + 4, + nl() + meta + .items + .iter() + .map(|(k, v)| text(k) + const_text(" = ") + v.render()) + .reduce(|acc, item| acc + nl() + item) + .unwrap_or(Document::Empty), + ) + nl(); + doc + const_text("(") + (singleline_items | multiline_items) + const_text(")") + }, + } + } +} + +impl Spanned for Attribute { + fn span(&self) -> SourceSpan { + match self { + Self::Marker(id) => id.span(), + Self::List(list) => list.span(), + Self::KeyValue(kv) => kv.span(), + } + } +} + +impl From for Attribute { + fn from(value: Ident) -> Self { + Self::Marker(value) + } +} + +impl From<(K, V)> for Attribute +where + K: Into, + V: Into, +{ + fn from(kv: (K, V)) -> Self { + let (key, value) = kv; + Self::List(MetaList { + span: SourceSpan::default(), + name: key.into(), + items: vec![value.into()], + }) + } +} + +impl From for Attribute { + fn from(value: MetaList) -> Self { + Self::List(value) + } +} + +impl From for Attribute { + fn from(value: MetaKeyValue) -> Self { + Self::KeyValue(value) + } +} diff --git a/assembly/src/ast/attribute/set.rs b/assembly/src/ast/attribute/set.rs new file mode 100644 index 000000000..bbf69698a --- /dev/null +++ b/assembly/src/ast/attribute/set.rs @@ -0,0 +1,239 @@ +use alloc::vec::Vec; +use core::fmt; + +use super::*; +use crate::ast::Ident; + +/// An [AttributeSet] provides storage and access to all of the attributes attached to a Miden +/// Assembly item, e.g. procedure definition. +/// +/// Attributes are uniqued by name, so if you attempt to add multiple attributes with the same name, +/// the last write wins. In Miden Assembly syntax, multiple key-value attributes are merged +/// automatically, and a syntax error is only generated when keys conflict. All other attribute +/// types produce an error if they are declared multiple times on the same item. +#[derive(Default, Clone, PartialEq, Eq)] +pub struct AttributeSet { + /// The attributes in this set. + /// + /// The [AttributeSet] structure has map-like semantics, so why are we using a vector here? + /// + /// * We expect attributes to be relatively rare, with no more than a handful on the same item + /// at any given time. + /// * A vector is much more space and time efficient to search for small numbers of items + /// * We can acheive map-like semantics without O(N) complexity by keeping the vector sorted by + /// the attribute name, and using binary search to search it. This gives us O(1) best-case + /// performance, and O(log N) in the worst case. + attrs: Vec, +} + +impl AttributeSet { + /// Create a new [AttributeSet] from `attrs` + /// + /// If the input attributes have duplicate entries for the same name, only one will be selected, + /// but it is unspecified which. + pub fn new(attrs: I) -> Self + where + I: IntoIterator, + { + let mut this = Self { attrs: attrs.into_iter().collect() }; + this.attrs.sort_by_key(|attr| attr.id()); + this.attrs.dedup_by_key(|attr| attr.id()); + this + } + + /// Returns true if there are no attributes in this set + #[inline] + pub fn is_empty(&self) -> bool { + self.attrs.is_empty() + } + + /// Returns the number of attributes in this set + #[inline] + pub fn len(&self) -> usize { + self.attrs.len() + } + + /// Check if this set has an attributed named `name` + pub fn has(&self, name: impl AsRef) -> bool { + self.get(name).is_some() + } + + /// Get the attribute named `name`, if one is present. + pub fn get(&self, name: impl AsRef) -> Option<&Attribute> { + let name = name.as_ref(); + match self.attrs.binary_search_by_key(&name, |attr| attr.name()) { + Ok(index) => self.attrs.get(index), + Err(_) => None, + } + } + + /// Get a mutable reference to the attribute named `name`, if one is present. + pub fn get_mut(&mut self, name: impl AsRef) -> Option<&mut Attribute> { + let name = name.as_ref(); + match self.attrs.binary_search_by_key(&name, |attr| attr.name()) { + Ok(index) => self.attrs.get_mut(index), + Err(_) => None, + } + } + + /// Get an iterator over the attributes in this set + #[inline] + pub fn iter(&self) -> core::slice::Iter<'_, Attribute> { + self.attrs.iter() + } + + /// Get a mutable iterator over the attributes in this set + #[inline] + pub fn iter_mut(&mut self) -> core::slice::IterMut<'_, Attribute> { + self.attrs.iter_mut() + } + + /// Insert `attr` in the attribute set, replacing any existing attribute with the same name + /// + /// Returns true if the insertion was new, or false if the insertion replaced an existing entry. + pub fn insert(&mut self, attr: Attribute) -> bool { + let name = attr.name(); + match self.attrs.binary_search_by_key(&name, |attr| attr.name()) { + Ok(index) => { + // Replace existing attribute + self.attrs[index] = attr; + false + }, + Err(index) => { + self.attrs.insert(index, attr); + true + }, + } + } + + /// Insert `attr` in the attribute set, but only if there is no existing attribute with the same + /// name. + /// + /// Returns `Err` with `attr` if there is already an existing attribute with the same name. + pub fn insert_new(&mut self, attr: Attribute) -> Result<(), Attribute> { + if self.has(attr.name()) { + Err(attr) + } else { + self.insert(attr); + Ok(()) + } + } + + /// Removes the attribute named `name`, if present. + pub fn remove(&mut self, name: impl AsRef) -> Option { + let name = name.as_ref(); + match self.attrs.binary_search_by_key(&name, |attr| attr.name()) { + Ok(index) => Some(self.attrs.remove(index)), + Err(_) => None, + } + } + + /// Gets the given key's corresponding entry in the set for in-place modfication + pub fn entry(&mut self, key: Ident) -> AttributeSetEntry<'_> { + match self.attrs.binary_search_by_key(&key.as_str(), |attr| attr.name()) { + Ok(index) => AttributeSetEntry::occupied(self, index), + Err(index) => AttributeSetEntry::vacant(self, key, index), + } + } + + /// Clear all attributes from the set + #[inline] + pub fn clear(&mut self) { + self.attrs.clear(); + } +} + +impl fmt::Debug for AttributeSet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut builder = f.debug_map(); + for attr in self.iter() { + match attr.metadata() { + None => { + builder.entry(&attr.name(), &"None"); + }, + Some(meta) => { + builder.entry(&attr.name(), &meta); + }, + } + } + builder.finish() + } +} + +impl FromIterator for AttributeSet { + #[inline] + fn from_iter>(iter: T) -> Self { + Self::new(iter) + } +} + +impl Extend for AttributeSet { + fn extend>(&mut self, iter: T) { + for attr in iter { + self.insert(attr); + } + } +} + +/// Represents an entry under a specific key in a [AttributeSet] +pub enum AttributeSetEntry<'a> { + /// The entry is currently occupied with a value + Occupied(AttributeSetOccupiedEntry<'a>), + /// The entry is currently vacant + Vacant(AttributeSetVacantEntry<'a>), +} +impl<'a> AttributeSetEntry<'a> { + fn occupied(set: &'a mut AttributeSet, index: usize) -> Self { + Self::Occupied(AttributeSetOccupiedEntry { set, index }) + } + + fn vacant(set: &'a mut AttributeSet, key: Ident, index: usize) -> Self { + Self::Vacant(AttributeSetVacantEntry { set, key, index }) + } +} + +#[doc(hidden)] +pub struct AttributeSetOccupiedEntry<'a> { + set: &'a mut AttributeSet, + index: usize, +} +impl<'a> AttributeSetOccupiedEntry<'a> { + #[inline] + pub fn get(&self) -> &Attribute { + &self.set.attrs[self.index] + } + + #[inline] + pub fn get_mut(&mut self) -> &mut Attribute { + &mut self.set.attrs[self.index] + } + + pub fn insert(self, attr: Attribute) { + if attr.name() != self.get().name() { + self.set.insert(attr); + } else { + self.set.attrs[self.index] = attr; + } + } + + #[inline] + pub fn remove(self) -> Attribute { + self.set.attrs.remove(self.index) + } +} + +#[doc(hidden)] +pub struct AttributeSetVacantEntry<'a> { + set: &'a mut AttributeSet, + key: Ident, + index: usize, +} +impl<'a> AttributeSetVacantEntry<'a> { + pub fn insert(self, attr: Attribute) { + if self.key != attr.id() { + self.set.insert(attr); + } else { + self.set.attrs.insert(self.index, attr); + } + } +} diff --git a/assembly/src/ast/mod.rs b/assembly/src/ast/mod.rs index 256173191..a606d9e90 100644 --- a/assembly/src/ast/mod.rs +++ b/assembly/src/ast/mod.rs @@ -1,5 +1,6 @@ //! Abstract syntax tree (AST) components of Miden programs, modules, and procedures. +mod attribute; mod block; mod constants; mod form; @@ -16,6 +17,10 @@ mod tests; pub mod visit; pub use self::{ + attribute::{ + Attribute, AttributeSet, AttributeSetEntry, BorrowedMeta, Meta, MetaExpr, MetaItem, + MetaKeyValue, MetaList, + }, block::Block, constants::{Constant, ConstantExpr, ConstantOp}, form::Form, diff --git a/assembly/src/ast/procedure/mod.rs b/assembly/src/ast/procedure/mod.rs index e74bd1fc2..8878018cf 100644 --- a/assembly/src/ast/procedure/mod.rs +++ b/assembly/src/ast/procedure/mod.rs @@ -14,7 +14,10 @@ pub use self::{ procedure::{Procedure, Visibility}, resolver::{LocalNameResolver, ResolvedProcedure}, }; -use crate::{ast::Invoke, SourceSpan, Span, Spanned}; +use crate::{ + ast::{AttributeSet, Invoke}, + SourceSpan, Span, Spanned, +}; // EXPORT // ================================================================================================ @@ -56,6 +59,14 @@ impl Export { } } + /// Returns the attributes for this procedure. + pub fn attributes(&self) -> Option<&AttributeSet> { + match self { + Self::Procedure(ref proc) => Some(proc.attributes()), + Self::Alias(_) => None, + } + } + /// Returns the visibility of this procedure (e.g. public or private). /// /// See [Visibility] for more details on what visibilities are supported. diff --git a/assembly/src/ast/procedure/procedure.rs b/assembly/src/ast/procedure/procedure.rs index 2555af296..70a0ac6a9 100644 --- a/assembly/src/ast/procedure/procedure.rs +++ b/assembly/src/ast/procedure/procedure.rs @@ -3,7 +3,7 @@ use core::fmt; use super::ProcedureName; use crate::{ - ast::{Block, Invoke}, + ast::{Attribute, AttributeSet, Block, Invoke}, SourceSpan, Span, Spanned, }; @@ -55,6 +55,8 @@ pub struct Procedure { span: SourceSpan, /// The documentation attached to this procedure docs: Option>, + /// The attributes attached to this procedure + attrs: AttributeSet, /// The local name of this procedure name: ProcedureName, /// The visibility of this procedure (i.e. whether it is exported or not) @@ -81,6 +83,7 @@ impl Procedure { Self { span, docs: None, + attrs: Default::default(), name, visibility, num_locals, @@ -95,6 +98,15 @@ impl Procedure { self } + /// Adds attributes to this procedure definition + pub fn with_attributes(mut self, attrs: I) -> Self + where + I: IntoIterator, + { + self.attrs.extend(attrs); + self + } + /// Modifies the visibility of this procedure. /// /// This is made crate-local as the visibility of a procedure is virtually always determined @@ -134,6 +146,30 @@ impl Procedure { self.docs.as_ref() } + /// Get the attributes attached to this procedure + #[inline] + pub fn attributes(&self) -> &AttributeSet { + &self.attrs + } + + /// Get the attributes attached to this procedure, mutably + #[inline] + pub fn attributes_mut(&mut self) -> &mut AttributeSet { + &mut self.attrs + } + + /// Returns true if this procedure has an attribute named `name` + #[inline] + pub fn has_attribute(&self, name: impl AsRef) -> bool { + self.attrs.has(name) + } + + /// Returns the attribute named `name`, if present + #[inline] + pub fn get_attribute(&self, name: impl AsRef) -> Option<&Attribute> { + self.attrs.get(name) + } + /// Returns a reference to the [Block] containing the body of this procedure. pub fn body(&self) -> &Block { &self.body @@ -216,6 +252,15 @@ impl crate::prettier::PrettyPrint for Procedure { .unwrap_or(Document::Empty); } + if !self.attrs.is_empty() { + doc = self + .attrs + .iter() + .map(|attr| attr.render()) + .reduce(|acc, attr| acc + nl() + attr) + .unwrap_or(Document::Empty); + } + doc += display(self.visibility) + const_text(".") + display(&self.name); if self.num_locals > 0 { doc += const_text(".") + display(self.num_locals); @@ -231,6 +276,7 @@ impl fmt::Debug for Procedure { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Procedure") .field("docs", &self.docs) + .field("attrs", &self.attrs) .field("name", &self.name) .field("visibility", &self.visibility) .field("num_locals", &self.num_locals) @@ -248,6 +294,7 @@ impl PartialEq for Procedure { && self.visibility == other.visibility && self.num_locals == other.num_locals && self.body == other.body + && self.attrs == other.attrs && self.docs == other.docs } } diff --git a/assembly/src/ast/tests.rs b/assembly/src/ast/tests.rs index 50ef338a8..7fff35c58 100644 --- a/assembly/src/ast/tests.rs +++ b/assembly/src/ast/tests.rs @@ -11,6 +11,12 @@ use crate::{ Felt, Span, }; +macro_rules! id { + ($name:ident) => { + Ident::new(stringify!($name)).unwrap() + }; +} + macro_rules! inst { ($inst:ident($value:expr)) => { Op::Inst(Span::unknown(Instruction::$inst($value))) @@ -145,7 +151,20 @@ macro_rules! proc { ))) }; - ($docs:expr, $name:ident, $num_locals:literal, $body:expr) => { + ([$($attr:expr),*], $name:ident, $num_locals:literal, $body:expr) => { + Form::Procedure(Export::Procedure( + Procedure::new( + Default::default(), + Visibility::Private, + stringify!($name).parse().expect("invalid procedure name"), + $num_locals, + $body, + ) + .with_attributes([$($attr),*]), + )) + }; + + ($docs:literal, $name:ident, $num_locals:literal, $body:expr) => { Form::Procedure(Export::Procedure( Procedure::new( Default::default(), @@ -157,6 +176,20 @@ macro_rules! proc { .with_docs(Some(Span::unknown($docs.to_string()))), )) }; + + ($docs:literal, [$($attr:expr),*], $name:ident, $num_locals:literal, $body:expr) => { + Form::Procedure(Export::Procedure( + Procedure::new( + Default::default(), + Visibility::Private, + stringify!($name).parse().expect("invalid procedure name"), + $num_locals, + $body, + ) + .with_docs($docs) + .with_attributes([$($attr),*]), + )) + }; } macro_rules! export { @@ -569,7 +602,7 @@ fn test_ast_parsing_module_sequential_if() -> Result<(), Report> { } #[test] -fn parsed_while_if_body() { +fn test_ast_parsing_while_if_body() { let context = TestContext::new(); let source = source_file!( &context, @@ -599,6 +632,65 @@ fn parsed_while_if_body() { assert_forms!(context, source, forms); } +#[test] +fn test_ast_parsing_attributes() -> Result<(), Report> { + let context = TestContext::new(); + + let source = source_file!( + &context, + r#" + # Simple marker attribute + @inline + proc.foo.1 + loc_load.0 + end + + # List attribute + @inline(always) + proc.bar.2 + padw + end + + # Key value attributes of various kinds + @numbers(decimal = 1, hex = 0xdeadbeef) + @props(name = baz) + @props(string = "not a valid quoted identifier") + proc.baz.2 + padw + end + + begin + exec.foo + exec.bar + exec.baz + end"# + ); + + let inline = Attribute::Marker(id!(inline)); + let inline_always = Attribute::List(MetaList::new(id!(inline), [MetaExpr::Ident(id!(always))])); + let numbers = Attribute::new( + id!(numbers), + [(id!(decimal), MetaExpr::from(1u8)), (id!(hex), MetaExpr::from(0xdeadbeefu32))], + ); + let props = Attribute::new( + id!(props), + [ + (id!(name), MetaExpr::from(id!(baz))), + (id!(string), MetaExpr::from("not a valid quoted identifier")), + ], + ); + + let forms = module!( + proc!([inline], foo, 1, block!(inst!(LocLoad(0u16.into())))), + proc!([inline_always], bar, 2, block!(inst!(PadW))), + proc!([numbers, props], baz, 2, block!(inst!(PadW))), + begin!(exec!(foo), exec!(bar), exec!(baz)) + ); + assert_eq!(context.parse_forms(source)?, forms); + + Ok(()) +} + // PROCEDURE IMPORTS // ================================================================================================ @@ -1078,6 +1170,6 @@ fn assert_parsing_line_unexpected_token() { " : ^|^", " : `-- found a mul here", " `----", - r#" help: expected "begin", or "const", or "export", or "proc", or "use", or end of file, or doc comment"# + r#" help: expected "@", or "begin", or "const", or "export", or "proc", or "use", or end of file, or doc comment"# ); } diff --git a/assembly/src/parser/error.rs b/assembly/src/parser/error.rs index 5d8899447..4b0fd1bf2 100644 --- a/assembly/src/parser/error.rs +++ b/assembly/src/parser/error.rs @@ -234,6 +234,26 @@ pub enum ParsingError { #[label] span: SourceSpan, }, + #[error("conflicting attributes for procedure definition")] + #[diagnostic()] + AttributeConflict { + #[label( + "conflict occurs because an attribute with the same name has already been defined" + )] + span: SourceSpan, + #[label("previously defined here")] + prev: SourceSpan, + }, + #[error("conflicting key-value attributes for procedure definition")] + #[diagnostic()] + AttributeKeyValueConflict { + #[label( + "conflict occurs because a key with the same name has already been set in a previous declaration" + )] + span: SourceSpan, + #[label("previously defined here")] + prev: SourceSpan, + }, } impl ParsingError { diff --git a/assembly/src/parser/grammar.lalrpop b/assembly/src/parser/grammar.lalrpop index a8be8ce46..65b78ae42 100644 --- a/assembly/src/parser/grammar.lalrpop +++ b/assembly/src/parser/grammar.lalrpop @@ -1,6 +1,6 @@ use alloc::{ boxed::Box, - collections::{VecDeque, BTreeSet}, + collections::{VecDeque, BTreeSet, BTreeMap}, string::ToString, sync::Arc, vec::Vec, @@ -34,6 +34,7 @@ extern { bare_ident => Token::Ident(<&'input str>), const_ident => Token::ConstantIdent(<&'input str>), quoted_ident => Token::QuotedIdent(<&'input str>), + quoted_string => Token::QuotedString(<&'input str>), hex_value => Token::HexValue(), bin_value => Token::BinValue(), doc_comment => Token::DocComment(), @@ -192,22 +193,36 @@ extern { "u32xor" => Token::U32Xor, "while" => Token::While, "xor" => Token::Xor, + "@" => Token::At, "!" => Token::Bang, "::" => Token::ColonColon, "." => Token::Dot, + "," => Token::Comma, "=" => Token::Equal, "(" => Token::Lparen, + "[" => Token::Lbracket, "-" => Token::Minus, "+" => Token::Plus, "//" => Token::SlashSlash, "/" => Token::Slash, "*" => Token::Star, ")" => Token::Rparen, + "]" => Token::Rbracket, "->" => Token::Rstab, EOF => Token::Eof, } } + +// comma-delimited with at least one element +#[inline] +CommaDelimited: Vec = { + ",")*> => { + v.push(e); + v + } +}; + // dot-delimited with at least one element #[inline] DotDelimited: Vec = { @@ -290,6 +305,66 @@ Begin: Form = { } Proc: Form = { + =>? { + use alloc::collections::btree_map::Entry; + let attributes = proc.attributes_mut(); + for attr in annotations { + match attr { + Attribute::KeyValue(kv) => { + match attributes.entry(kv.id()) { + AttributeSetEntry::Vacant(entry) => { + entry.insert(Attribute::KeyValue(kv)); + } + AttributeSetEntry::Occupied(mut entry) => { + let value = entry.get_mut(); + match value { + Attribute::KeyValue(ref mut existing_kvs) => { + for (k, v) in kv.into_iter() { + let span = k.span(); + match existing_kvs.entry(k) { + Entry::Vacant(entry) => { + entry.insert(v); + } + Entry::Occupied(ref entry) => { + let prev = entry.get(); + return Err(ParseError::User { + error: ParsingError::AttributeKeyValueConflict { span, prev: prev.span() }, + }); + } + } + } + } + other => { + return Err(ParseError::User { + error: ParsingError::AttributeConflict { span: kv.span(), prev: other.span() }, + }); + } + } + } + } + } + attr => { + match attributes.entry(attr.id()) { + AttributeSetEntry::Vacant(entry) => { + entry.insert(attr); + } + AttributeSetEntry::Occupied(ref entry) => { + let prev_attr = entry.get(); + return Err(ParseError::User { + error: ParsingError::AttributeConflict { span: attr.span(), prev: prev_attr.span() }, + }); + } + } + } + } + } + Ok(Form::Procedure(Export::Procedure(proc))) + }, + AliasDef => Form::Procedure(Export::Alias(<>)), +} + +#[inline] +ProcedureDef: Procedure = { "." > "end" =>? { let num_locals = num_locals.unwrap_or(0); let procedure = Procedure::new( @@ -299,9 +374,12 @@ Proc: Form = { num_locals, body ); - Ok(Form::Procedure(Export::Procedure(procedure))) + Ok(procedure) }, +} +#[inline] +AliasDef: ProcedureAlias = { "export" "." " )?> =>? { let span = span!(source_file.id(), l, r); let alias = match name { @@ -341,7 +419,7 @@ Proc: Form = { ProcedureAlias::new(export_name, AliasTarget::AbsoluteProcedurePath(target)) } }; - Ok(Form::Procedure(Export::Alias(alias))) + Ok(alias) } } @@ -351,6 +429,68 @@ Visibility: Visibility = { "export" => Visibility::Public, } +// ANNOTATIONS +// ================================================================================================ + +Annotation: Attribute = { + "@" => attr.with_span(span!(source_file.id(), l, r)), +} + +#[inline] +Attribute: Attribute = { + "(" > ")" => { + Attribute::List(MetaList { span: span!(source_file.id(), l, r), name, items }) + }, + + "(" > ")" =>? { + use alloc::collections::btree_map::Entry; + + let mut map = BTreeMap::::default(); + for meta_kv in items { + let (span, (k, v)) = meta_kv.into_parts(); + match map.entry(k) { + Entry::Occupied(ref entry) => { + let prev = entry.key().span(); + return Err(ParseError::User { + error: ParsingError::AttributeKeyValueConflict { span, prev }, + }); + } + Entry::Vacant(entry) => { + entry.insert(v); + } + } + } + Ok(Attribute::KeyValue(MetaKeyValue { span: span!(source_file.id(), l, r), name, items: map })) + }, + + => Attribute::Marker(<>), +} + +MetaKeyValue: Span<(Ident, MetaExpr)> = { + "=" => { + let span = span!(source_file.id(), l, r); + Span::new(span, (key, value)) + } +} + +MetaExpr: MetaExpr = { + BareIdent => MetaExpr::Ident(<>), + QuotedString => MetaExpr::String(<>), + => MetaExpr::Int(Span::new(span!(source_file.id(), l, r), value)), +} + +#[inline] +QuotedString: Ident = { + => { + let value = interned.get(value).cloned().unwrap_or_else(|| { + let value = Arc::::from(value.to_string().into_boxed_str()); + interned.insert(value.clone()); + value + }); + Ident::new_unchecked(Span::new(span!(source_file.id(), l, r), value)) + } +} + // CODE BLOCKS // ================================================================================================ diff --git a/assembly/src/parser/lexer.rs b/assembly/src/parser/lexer.rs index b256addd7..0664ab0e9 100644 --- a/assembly/src/parser/lexer.rs +++ b/assembly/src/parser/lexer.rs @@ -274,15 +274,19 @@ impl<'input> Lexer<'input> { } match self.read() { + '@' => pop!(self, Token::At), '!' => pop!(self, Token::Bang), ':' => match self.peek() { ':' => pop2!(self, Token::ColonColon), _ => Err(ParsingError::InvalidToken { span: self.span() }), }, '.' => pop!(self, Token::Dot), + ',' => pop!(self, Token::Comma), '=' => pop!(self, Token::Equal), '(' => pop!(self, Token::Lparen), + '[' => pop!(self, Token::Lbracket), ')' => pop!(self, Token::Rparen), + ']' => pop!(self, Token::Rbracket), '-' => match self.peek() { '>' => pop2!(self, Token::Rstab), _ => pop!(self, Token::Minus), @@ -293,7 +297,7 @@ impl<'input> Lexer<'input> { _ => pop!(self, Token::Slash), }, '*' => pop!(self, Token::Star), - '"' => self.lex_quoted_identifier(), + '"' => self.lex_quoted_identifier_or_string(), '0' => match self.peek() { 'x' => { self.skip(); @@ -414,10 +418,11 @@ impl<'input> Lexer<'input> { } } - fn lex_quoted_identifier(&mut self) -> Result, ParsingError> { + fn lex_quoted_identifier_or_string(&mut self) -> Result, ParsingError> { // Skip quotation mark self.skip(); + let mut is_identifier = true; let quote_size = ByteOffset::from_char_len('"'); loop { match self.read() { @@ -426,27 +431,37 @@ impl<'input> Lexer<'input> { start: SourceSpan::at(self.source_id, self.span().start()), }); }, + '\\' => { + is_identifier = false; + self.skip(); + match self.read() { + '"' | '\n' => { + self.skip(); + }, + _ => (), + } + }, '"' => { let span = self.span(); let start = span.start() + quote_size; let span = SourceSpan::new(self.source_id, start..span.end()); self.skip(); - break Ok(Token::QuotedIdent(self.slice_span(span))); + break Ok(if is_identifier { + Token::QuotedIdent(self.slice_span(span)) + } else { + Token::QuotedString(self.slice_span(span)) + }); }, c if c.is_ascii_alphanumeric() => { self.skip(); - continue; }, '_' | '$' | '-' | '!' | '?' | '<' | '>' | ':' | '.' => { self.skip(); - continue; }, - c => { - let loc = self.span().end() - ByteOffset::from_char_len(c); - break Err(ParsingError::InvalidIdentCharacter { - span: SourceSpan::at(self.source_id, loc), - }); + _ => { + is_identifier = false; + self.skip(); }, } } diff --git a/assembly/src/parser/token.rs b/assembly/src/parser/token.rs index 7ff7976dd..825127871 100644 --- a/assembly/src/parser/token.rs +++ b/assembly/src/parser/token.rs @@ -50,6 +50,69 @@ pub enum HexEncodedValue { /// A set of 4 field elements, 32 bytes, encoded as a contiguous string of 64 hex digits Word([Felt; 4]), } +impl fmt::Display for HexEncodedValue { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::U8(value) => write!(f, "{value}"), + Self::U16(value) => write!(f, "{value}"), + Self::U32(value) => write!(f, "{value:#04x}"), + Self::Felt(value) => write!(f, "{:#08x}", &value.as_int().to_be()), + Self::Word(value) => write!( + f, + "{:#08x}{:08x}{:08x}{:08x}", + &value[0].as_int(), + &value[1].as_int(), + &value[2].as_int(), + &value[3].as_int(), + ), + } + } +} +impl PartialOrd for HexEncodedValue { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} +impl Ord for HexEncodedValue { + fn cmp(&self, other: &Self) -> core::cmp::Ordering { + use core::cmp::Ordering; + match (self, other) { + (Self::U8(l), Self::U8(r)) => l.cmp(r), + (Self::U8(_), _) => Ordering::Less, + (Self::U16(_), Self::U8(_)) => Ordering::Greater, + (Self::U16(l), Self::U16(r)) => l.cmp(r), + (Self::U16(_), _) => Ordering::Less, + (Self::U32(_), Self::U8(_) | Self::U16(_)) => Ordering::Greater, + (Self::U32(l), Self::U32(r)) => l.cmp(r), + (Self::U32(_), _) => Ordering::Less, + (Self::Felt(_), Self::U8(_) | Self::U16(_) | Self::U32(_)) => Ordering::Greater, + (Self::Felt(l), Self::Felt(r)) => l.as_int().cmp(&r.as_int()), + (Self::Felt(_), _) => Ordering::Less, + (Self::Word([l0, l1, l2, l3]), Self::Word([r0, r1, r2, r3])) => l0 + .as_int() + .cmp(&r0.as_int()) + .then_with(|| l1.as_int().cmp(&r1.as_int())) + .then_with(|| l2.as_int().cmp(&r2.as_int())) + .then_with(|| l3.as_int().cmp(&r3.as_int())), + (Self::Word(_), _) => Ordering::Greater, + } + } +} + +impl core::hash::Hash for HexEncodedValue { + fn hash(&self, state: &mut H) { + core::mem::discriminant(self).hash(state); + match self { + Self::U8(value) => value.hash(state), + Self::U16(value) => value.hash(state), + Self::U32(value) => value.hash(state), + Self::Felt(value) => value.as_int().hash(state), + Self::Word([a, b, c, d]) => { + [a.as_int(), b.as_int(), c.as_int(), d.as_int()].hash(state) + }, + } + } +} // BINARY ENCODED VALUE // ================================================================================================ @@ -225,17 +288,21 @@ pub enum Token<'input> { U32Xor, While, Xor, + At, Bang, ColonColon, Dot, + Comma, Equal, Lparen, + Lbracket, Minus, Plus, SlashSlash, Slash, Star, Rparen, + Rbracket, Rstab, DocComment(DocumentationType), HexValue(HexEncodedValue), @@ -244,6 +311,7 @@ pub enum Token<'input> { Ident(&'input str), ConstantIdent(&'input str), QuotedIdent(&'input str), + QuotedString(&'input str), Comment, Eof, } @@ -404,17 +472,21 @@ impl<'input> fmt::Display for Token<'input> { Token::U32Xor => write!(f, "u32xor"), Token::While => write!(f, "while"), Token::Xor => write!(f, "xor"), + Token::At => write!(f, "@"), Token::Bang => write!(f, "!"), Token::ColonColon => write!(f, "::"), Token::Dot => write!(f, "."), + Token::Comma => write!(f, ","), Token::Equal => write!(f, "="), Token::Lparen => write!(f, "("), + Token::Lbracket => write!(f, "["), Token::Minus => write!(f, "-"), Token::Plus => write!(f, "+"), Token::SlashSlash => write!(f, "//"), Token::Slash => write!(f, "/"), Token::Star => write!(f, "*"), Token::Rparen => write!(f, ")"), + Token::Rbracket => write!(f, "]"), Token::Rstab => write!(f, "->"), Token::DocComment(DocumentationType::Module(_)) => f.write_str("module doc"), Token::DocComment(DocumentationType::Form(_)) => f.write_str("doc comment"), @@ -424,6 +496,7 @@ impl<'input> fmt::Display for Token<'input> { Token::Ident(_) => f.write_str("identifier"), Token::ConstantIdent(_) => f.write_str("constant identifier"), Token::QuotedIdent(_) => f.write_str("quoted identifier"), + Token::QuotedString(_) => f.write_str("quoted string"), Token::Comment => f.write_str("comment"), Token::Eof => write!(f, "end of file"), } @@ -804,17 +877,21 @@ impl<'input> Token<'input> { Token::Ident(_) => { // Nope, try again match s { + "@" => Ok(Token::At), "!" => Ok(Token::Bang), "::" => Ok(Token::ColonColon), "." => Ok(Token::Dot), + "," => Ok(Token::Comma), "=" => Ok(Token::Equal), "(" => Ok(Token::Lparen), + "[" => Ok(Token::Lbracket), "-" => Ok(Token::Minus), "+" => Ok(Token::Plus), "//" => Ok(Token::SlashSlash), "/" => Ok(Token::Slash), "*" => Ok(Token::Star), ")" => Ok(Token::Rparen), + "]" => Ok(Token::Rbracket), "->" => Ok(Token::Rstab), "end of file" => Ok(Token::Eof), "module doc" => Ok(Token::DocComment(DocumentationType::Module(String::new()))), @@ -826,6 +903,7 @@ impl<'input> Token<'input> { "identifier" => Ok(Token::Ident("")), "constant identifier" => Ok(Token::ConstantIdent("")), "quoted identifier" => Ok(Token::QuotedIdent("")), + "quoted string" => Ok(Token::QuotedString("")), _ => Err(()), } }, diff --git a/assembly/src/tests.rs b/assembly/src/tests.rs index 7fb90cab2..d361e1c60 100644 --- a/assembly/src/tests.rs +++ b/assembly/src/tests.rs @@ -729,7 +729,7 @@ fn constant_must_be_valid_felt() -> TestResult { " : ^^^|^^^", " : `-- found a constant identifier here", " `----", - " help: expected \"*\", or \"+\", or \"-\", or \"/\", or \"//\", or \"begin\", or \"const\", \ + " help: expected \"*\", or \"+\", or \"-\", or \"/\", or \"//\", or \"@\", or \"begin\", or \"const\", \ or \"export\", or \"proc\", or \"use\", or end of file, or doc comment" ); Ok(()) @@ -1054,7 +1054,7 @@ fn decorators_repeat_split() -> TestResult { "\ begin trace.0 - repeat.2 + repeat.2 if.true trace.1 push.42 trace.2 else @@ -1695,7 +1695,7 @@ fn ensure_correct_procedure_selection_on_collision() -> TestResult { proc.f add end - + proc.g trace.2 add @@ -2325,7 +2325,7 @@ end"; " : `-- found a -> here", "3 |", " `----", - r#" help: expected "begin", or "const", or "export", or "proc", or "use", or end of file, or doc comment"# + r#" help: expected "@", or "begin", or "const", or "export", or "proc", or "use", or end of file, or doc comment"# ); // --- duplicate module import -------------------------------------------- @@ -2532,7 +2532,7 @@ fn invalid_empty_program() { "unexpected end of file", regex!(r#",-\[test[\d]+:1:1\]"#), "`----", - r#" help: expected "begin", or "const", or "export", or "proc", or "use", or doc comment"# + r#" help: expected "@", or "begin", or "const", or "export", or "proc", or "use", or doc comment"# ); assert_assembler_diagnostic!( @@ -2541,7 +2541,7 @@ fn invalid_empty_program() { "unexpected end of file", regex!(r#",-\[test[\d]+:1:1\]"#), " `----", - r#" help: expected "begin", or "const", or "export", or "proc", or "use", or doc comment"# + r#" help: expected "@", or "begin", or "const", or "export", or "proc", or "use", or doc comment"# ); } @@ -2557,7 +2557,7 @@ fn invalid_program_unrecognized_token() { " : ^^|^", " : `-- found a identifier here", " `----", - r#" help: expected "begin", or "const", or "export", or "proc", or "use", or doc comment"# + r#" help: expected "@", or "begin", or "const", or "export", or "proc", or "use", or doc comment"# ); } @@ -2587,7 +2587,7 @@ fn invalid_program_invalid_top_level_token() { " : ^|^", " : `-- found a mul here", " `----", - r#" help: expected "begin", or "const", or "export", or "proc", or "use", or end of file, or doc comment"# + r#" help: expected "@", or "begin", or "const", or "export", or "proc", or "use", or end of file, or doc comment"# ); }