Annotations syntax and per-file default units preparatory work (#4822)

* Parse annotations

Signed-off-by: Nick Cameron <nrc@ncameron.org>

* Propagate settings from annotations to exec_state

Signed-off-by: Nick Cameron <nrc@ncameron.org>

---------

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2024-12-17 15:23:00 +13:00
committed by GitHub
parent 3dd98ae1d5
commit f165d19fda
7 changed files with 399 additions and 93 deletions

View File

@ -0,0 +1,73 @@
//! Data on available annotations.
use super::kcl_value::{UnitAngle, UnitLen};
use crate::{
errors::KclErrorDetails,
parsing::ast::types::{Expr, Node, NonCodeValue, ObjectProperty},
KclError, SourceRange,
};
pub(super) const SETTINGS: &str = "settings";
pub(super) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
pub(super) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
pub(super) fn expect_properties<'a>(
for_key: &'static str,
annotation: &'a NonCodeValue,
source_range: SourceRange,
) -> Result<&'a [Node<ObjectProperty>], KclError> {
match annotation {
NonCodeValue::Annotation { name, properties } => {
assert_eq!(name.name, for_key);
Ok(&**properties.as_ref().ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Empty `{for_key}` annotation"),
source_ranges: vec![source_range],
})
})?)
}
_ => unreachable!(),
}
}
pub(super) fn expect_ident(expr: &Expr) -> Result<&str, KclError> {
match expr {
Expr::Identifier(id) => Ok(&id.name),
e => Err(KclError::Semantic(KclErrorDetails {
message: "Unexpected settings value, expected a simple name, e.g., `mm`".to_owned(),
source_ranges: vec![e.into()],
})),
}
}
impl UnitLen {
pub(super) fn from_str(s: &str, source_range: SourceRange) -> Result<Self, KclError> {
match s {
"mm" => Ok(UnitLen::Mm),
"cm" => Ok(UnitLen::Cm),
"m" => Ok(UnitLen::M),
"inch" | "in" => Ok(UnitLen::Inches),
"ft" => Ok(UnitLen::Feet),
"yd" => Ok(UnitLen::Yards),
value => Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Unexpected settings value: `{value}`; expected one of `mm`, `cm`, `m`, `inch`, `ft`, `yd`"
),
source_ranges: vec![source_range],
})),
}
}
}
impl UnitAngle {
pub(super) fn from_str(s: &str, source_range: SourceRange) -> Result<Self, KclError> {
match s {
"deg" => Ok(UnitAngle::Degrees),
"rad" => Ok(UnitAngle::Radians),
value => Err(KclError::Semantic(KclErrorDetails {
message: format!("Unexpected settings value: `{value}`; expected one of `deg`, `rad`"),
source_ranges: vec![source_range],
})),
}
}
}

View File

@ -8,7 +8,10 @@ use crate::{
errors::KclErrorDetails,
exec::{ProgramMemory, Sketch},
execution::{Face, ImportedGeometry, MemoryFunction, Metadata, Plane, SketchSet, Solid, SolidSet, TagIdentifier},
parsing::ast::types::{FunctionExpression, KclNone, LiteralValue, TagDeclarator, TagNode},
parsing::{
ast::types::{FunctionExpression, KclNone, LiteralValue, TagDeclarator, TagNode},
token::NumericSuffix,
},
std::{args::Arg, FnAsArg},
ExecState, ExecutorContext, KclError, ModuleId, SourceRange,
};
@ -561,3 +564,52 @@ impl KclValue {
}
}
}
// TODO called UnitLen so as not to clash with UnitLength in settings)
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)]
#[serde(tag = "type")]
pub enum UnitLen {
Mm,
Cm,
M,
Inches,
Feet,
Yards,
}
impl TryFrom<NumericSuffix> for UnitLen {
type Error = ();
fn try_from(suffix: NumericSuffix) -> std::result::Result<Self, Self::Error> {
match suffix {
NumericSuffix::Mm => Ok(Self::Mm),
NumericSuffix::Cm => Ok(Self::Cm),
NumericSuffix::M => Ok(Self::M),
NumericSuffix::Inch => Ok(Self::Inches),
NumericSuffix::Ft => Ok(Self::Feet),
NumericSuffix::Yd => Ok(Self::Yards),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq)]
#[ts(export)]
#[serde(tag = "type")]
pub enum UnitAngle {
Degrees,
Radians,
}
impl TryFrom<NumericSuffix> for UnitAngle {
type Error = ();
fn try_from(suffix: NumericSuffix) -> std::result::Result<Self, Self::Error> {
match suffix {
NumericSuffix::Deg => Ok(Self::Degrees),
NumericSuffix::Rad => Ok(Self::Radians),
_ => Err(()),
}
}
}

View File

@ -24,6 +24,7 @@ pub use function_param::FunctionParam;
pub use kcl_value::{KclObjectFields, KclValue};
use uuid::Uuid;
mod annotations;
pub(crate) mod cache;
mod cad_op;
mod exec_ast;
@ -36,8 +37,8 @@ use crate::{
execution::cache::{CacheInformation, CacheResult},
fs::{FileManager, FileSystem},
parsing::ast::types::{
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, Program as AstProgram,
TagDeclarator, TagNode,
BodyItem, Expr, FunctionExpression, ImportSelector, ItemVisibility, Node, NodeRef, NonCodeValue,
Program as AstProgram, TagDeclarator, TagNode,
},
settings::types::UnitLength,
source_range::{ModuleId, SourceRange},
@ -88,6 +89,8 @@ pub struct ModuleState {
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
pub operations: Vec<Operation>,
/// Settings specified from annotations.
pub settings: MetaSettings,
}
impl Default for ExecState {
@ -186,6 +189,56 @@ impl GlobalState {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
pub struct MetaSettings {
pub default_length_units: kcl_value::UnitLen,
pub default_angle_units: kcl_value::UnitAngle,
}
impl Default for MetaSettings {
fn default() -> Self {
MetaSettings {
default_length_units: kcl_value::UnitLen::Mm,
default_angle_units: kcl_value::UnitAngle::Degrees,
}
}
}
impl MetaSettings {
fn update_from_annotation(&mut self, annotation: &NonCodeValue, source_range: SourceRange) -> Result<(), KclError> {
let properties = annotations::expect_properties(annotations::SETTINGS, annotation, source_range)?;
for p in properties {
match &*p.inner.key.name {
annotations::SETTINGS_UNIT_LENGTH => {
let value = annotations::expect_ident(&p.inner.value)?;
let value = kcl_value::UnitLen::from_str(value, source_range)?;
self.default_length_units = value;
}
annotations::SETTINGS_UNIT_ANGLE => {
let value = annotations::expect_ident(&p.inner.value)?;
let value = kcl_value::UnitAngle::from_str(value, source_range)?;
self.default_angle_units = value;
}
name => {
return Err(KclError::Semantic(KclErrorDetails {
message: format!(
"Unexpected settings key: `{name}`; expected one of `{}`, `{}`",
annotations::SETTINGS_UNIT_LENGTH,
annotations::SETTINGS_UNIT_ANGLE
),
source_ranges: vec![source_range],
}))
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(rename_all = "camelCase")]
@ -2020,6 +2073,22 @@ impl ExecutorContext {
exec_state: &mut ExecState,
body_type: BodyType,
) -> Result<Option<KclValue>, KclError> {
if let Some((annotation, source_range)) = program
.non_code_meta
.start_nodes
.iter()
.filter_map(|n| {
n.annotation(annotations::SETTINGS)
.map(|result| (result, n.as_source_range()))
})
.next()
{
exec_state
.mod_local
.settings
.update_from_annotation(annotation, source_range)?;
}
let mut last_expr = None;
// Iterate over the body of the program.
for statement in &program.body {

View File

@ -1000,52 +1000,22 @@ pub struct NonCodeNode {
pub digest: Option<Digest>,
}
impl Node<NonCodeNode> {
pub fn format(&self, indentation: &str) -> String {
match &self.value {
NonCodeValue::InlineComment {
value,
style: CommentStyle::Line,
} => format!(" // {}\n", value),
NonCodeValue::InlineComment {
value,
style: CommentStyle::Block,
} => format!(" /* {} */", value),
NonCodeValue::BlockComment { value, style } => match style {
CommentStyle::Block => format!("{}/* {} */", indentation, value),
CommentStyle::Line => {
if value.trim().is_empty() {
format!("{}//\n", indentation)
} else {
format!("{}// {}\n", indentation, value.trim())
}
}
},
NonCodeValue::NewLineBlockComment { value, style } => {
let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
match style {
CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
CommentStyle::Line => {
if value.trim().is_empty() {
format!("{}{}//\n", add_start_new_line, indentation)
} else {
format!("{}{}// {}\n", add_start_new_line, indentation, value.trim())
}
}
}
}
NonCodeValue::NewLine => "\n\n".to_string(),
}
}
}
impl NonCodeNode {
#[cfg(test)]
pub fn value(&self) -> String {
match &self.value {
NonCodeValue::InlineComment { value, style: _ } => value.clone(),
NonCodeValue::BlockComment { value, style: _ } => value.clone(),
NonCodeValue::NewLineBlockComment { value, style: _ } => value.clone(),
NonCodeValue::NewLine => "\n\n".to_string(),
NonCodeValue::Annotation { name, .. } => name.name.clone(),
}
}
pub fn annotation(&self, expected_name: &str) -> Option<&NonCodeValue> {
match &self.value {
a @ NonCodeValue::Annotation { name, .. } if name.name == expected_name => Some(a),
_ => None,
}
}
}
@ -1063,6 +1033,7 @@ pub enum CommentStyle {
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type", rename_all = "camelCase")]
#[allow(clippy::large_enum_variant)]
pub enum NonCodeValue {
/// An inline comment.
/// Here are examples:
@ -1095,6 +1066,10 @@ pub enum NonCodeValue {
// A new line like `\n\n` NOT a new line like `\n`.
// This is also not a comment.
NewLine,
Annotation {
name: Node<Identifier>,
properties: Option<Vec<Node<ObjectProperty>>>,
},
}
#[derive(Debug, Default, Clone, Serialize, PartialEq, ts_rs::TS, JsonSchema)]

View File

@ -283,38 +283,86 @@ fn non_code_node(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
alt((non_code_node_leading_whitespace, non_code_node_no_leading_whitespace)).parse_next(i)
}
fn annotation(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
let at = at_sign.parse_next(i)?;
let name = binding_name.parse_next(i)?;
let mut end = name.end;
let properties = if peek(open_paren).parse_next(i).is_ok() {
open_paren(i)?;
ignore_whitespace(i);
let properties: Vec<_> = separated(
0..,
separated_pair(
terminated(identifier, opt(whitespace)),
terminated(one_of((TokenType::Operator, "=")), opt(whitespace)),
expression,
)
.map(|(key, value)| Node {
start: key.start,
end: value.end(),
module_id: key.module_id,
inner: ObjectProperty {
key,
value,
digest: None,
},
}),
comma_sep,
)
.parse_next(i)?;
ignore_trailing_comma(i);
ignore_whitespace(i);
end = close_paren(i)?.end;
Some(properties)
} else {
None
};
let value = NonCodeValue::Annotation { name, properties };
Ok(Node::new(
NonCodeNode { value, digest: None },
at.start,
end,
at.module_id,
))
}
// Matches remaining three cases of NonCodeValue
fn non_code_node_no_leading_whitespace(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
any.verify_map(|token: Token| {
if token.is_code_token() {
None
} else {
let value = match token.token_type {
TokenType::Whitespace if token.value.contains("\n\n") => NonCodeValue::NewLine,
TokenType::LineComment => NonCodeValue::BlockComment {
value: token.value.trim_start_matches("//").trim().to_owned(),
style: CommentStyle::Line,
},
TokenType::BlockComment => NonCodeValue::BlockComment {
style: CommentStyle::Block,
value: token
.value
.trim_start_matches("/*")
.trim_end_matches("*/")
.trim()
.to_owned(),
},
_ => return None,
};
Some(Node::new(
NonCodeNode { value, digest: None },
token.start,
token.end,
token.module_id,
))
}
})
.context(expected("Non-code token (comments or whitespace)"))
alt((
annotation,
any.verify_map(|token: Token| {
if token.is_code_token() {
None
} else {
let value = match token.token_type {
TokenType::Whitespace if token.value.contains("\n\n") => NonCodeValue::NewLine,
TokenType::LineComment => NonCodeValue::BlockComment {
value: token.value.trim_start_matches("//").trim().to_owned(),
style: CommentStyle::Line,
},
TokenType::BlockComment => NonCodeValue::BlockComment {
style: CommentStyle::Block,
value: token
.value
.trim_start_matches("/*")
.trim_end_matches("*/")
.trim()
.to_owned(),
},
_ => return None,
};
Some(Node::new(
NonCodeNode { value, digest: None },
token.start,
token.end,
token.module_id,
))
}
})
.context(expected("Non-code token (comments or whitespace)")),
))
.parse_next(i)
}
@ -1191,6 +1239,7 @@ fn noncode_just_after_code(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
x @ NonCodeValue::InlineComment { .. } => x,
x @ NonCodeValue::NewLineBlockComment { .. } => x,
x @ NonCodeValue::NewLine => x,
x @ NonCodeValue::Annotation { .. } => x,
};
Node::new(
NonCodeNode { value, ..nc.inner },
@ -1211,6 +1260,7 @@ fn noncode_just_after_code(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
x @ NonCodeValue::InlineComment { .. } => x,
x @ NonCodeValue::NewLineBlockComment { .. } => x,
x @ NonCodeValue::NewLine => x,
x @ NonCodeValue::Annotation { .. } => x,
};
Node::new(NonCodeNode { value, ..nc.inner }, nc.start, nc.end, nc.module_id)
}
@ -1250,7 +1300,7 @@ fn body_items_within_function(i: &mut TokenSlice) -> PResult<WithinFunction> {
(import_stmt.map(BodyItem::ImportStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
Token { ref value, .. } if value == "return" =>
(return_stmt.map(BodyItem::ReturnStatement), opt(noncode_just_after_code)).map(WithinFunction::BodyItem),
token if !token.is_code_token() => {
token if !token.is_code_token() || token.token_type == TokenType::At => {
non_code_node.map(WithinFunction::NonCode)
},
_ =>
@ -2267,9 +2317,8 @@ fn question_mark(i: &mut TokenSlice) -> PResult<()> {
Ok(())
}
fn at_sign(i: &mut TokenSlice) -> PResult<()> {
TokenType::At.parse_from(i)?;
Ok(())
fn at_sign(i: &mut TokenSlice) -> PResult<Token> {
TokenType::At.parse_from(i)
}
fn fun(i: &mut TokenSlice) -> PResult<Token> {
@ -3626,6 +3675,22 @@ height = [obj["a"] -1, 0]"#;
crate::parsing::top_level_parse("foo(42, fn(x) { return x + 1 })").unwrap();
}
#[test]
fn test_annotation_fn() {
crate::parsing::top_level_parse(
r#"fn foo() {
@annotated
return 1
}"#,
)
.unwrap();
}
#[test]
fn test_annotation_settings() {
crate::parsing::top_level_parse("@settings(units = mm)").unwrap();
}
#[test]
fn test_anon_fn_no_fn() {
assert_err_contains("foo(42, (x) { return x + 1 })", "Anonymous function requires `fn`");

View File

@ -56,14 +56,13 @@ impl NumericSuffix {
impl FromStr for NumericSuffix {
type Err = CompilationError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"_" => Ok(NumericSuffix::Count),
"mm" => Ok(NumericSuffix::Mm),
"cm" => Ok(NumericSuffix::Cm),
"m" => Ok(NumericSuffix::M),
"inch" => Ok(NumericSuffix::Inch),
"in" => Ok(NumericSuffix::Inch),
"inch" | "in" => Ok(NumericSuffix::Inch),
"ft" => Ok(NumericSuffix::Ft),
"yd" => Ok(NumericSuffix::Yd),
"deg" => Ok(NumericSuffix::Deg),

View File

@ -3,10 +3,10 @@ use std::fmt::Write;
use crate::parsing::{
ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
CallExpressionKw, DefaultParamVal, Expr, FnArgType, FormatOptions, FunctionExpression, IfExpression,
ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier, LiteralValue,
MemberExpression, MemberObject, Node, NonCodeValue, ObjectExpression, Parameter, PipeExpression, Program,
TagDeclarator, UnaryExpression, VariableDeclaration, VariableKind,
CallExpressionKw, CommentStyle, DefaultParamVal, Expr, FnArgType, FormatOptions, FunctionExpression,
IfExpression, ImportSelector, ImportStatement, ItemVisibility, LabeledArg, Literal, LiteralIdentifier,
LiteralValue, MemberExpression, MemberObject, Node, NonCodeNode, NonCodeValue, ObjectExpression, Parameter,
PipeExpression, Program, TagDeclarator, UnaryExpression, VariableDeclaration, VariableKind,
},
PIPE_OPERATOR,
};
@ -55,7 +55,7 @@ impl Program {
self.non_code_meta
.start_nodes
.iter()
.map(|start| start.format(&indentation))
.map(|start| start.recast(options, indentation_level))
.collect()
}
} else {
@ -76,7 +76,7 @@ impl Program {
.iter()
.enumerate()
.map(|(i, custom_white_space_or_comment)| {
let formatted = custom_white_space_or_comment.format(&indentation);
let formatted = custom_white_space_or_comment.recast(options, indentation_level);
if i == 0 && !formatted.trim().is_empty() {
if let NonCodeValue::BlockComment { .. } = custom_white_space_or_comment.value {
format!("\n{}", formatted)
@ -115,7 +115,75 @@ impl NonCodeValue {
fn should_cause_array_newline(&self) -> bool {
match self {
Self::InlineComment { .. } => false,
Self::BlockComment { .. } | Self::NewLineBlockComment { .. } | Self::NewLine => true,
Self::BlockComment { .. } | Self::NewLineBlockComment { .. } | Self::NewLine | Self::Annotation { .. } => {
true
}
}
}
}
impl Node<NonCodeNode> {
fn recast(&self, options: &FormatOptions, indentation_level: usize) -> String {
let indentation = options.get_indentation(indentation_level);
match &self.value {
NonCodeValue::InlineComment {
value,
style: CommentStyle::Line,
} => format!(" // {}\n", value),
NonCodeValue::InlineComment {
value,
style: CommentStyle::Block,
} => format!(" /* {} */", value),
NonCodeValue::BlockComment { value, style } => match style {
CommentStyle::Block => format!("{}/* {} */", indentation, value),
CommentStyle::Line => {
if value.trim().is_empty() {
format!("{}//\n", indentation)
} else {
format!("{}// {}\n", indentation, value.trim())
}
}
},
NonCodeValue::NewLineBlockComment { value, style } => {
let add_start_new_line = if self.start == 0 { "" } else { "\n\n" };
match style {
CommentStyle::Block => format!("{}{}/* {} */\n", add_start_new_line, indentation, value),
CommentStyle::Line => {
if value.trim().is_empty() {
format!("{}{}//\n", add_start_new_line, indentation)
} else {
format!("{}{}// {}\n", add_start_new_line, indentation, value.trim())
}
}
}
}
NonCodeValue::NewLine => "\n\n".to_string(),
NonCodeValue::Annotation { name, properties } => {
let mut result = "@".to_owned();
result.push_str(&name.name);
if let Some(properties) = properties {
result.push('(');
result.push_str(
&properties
.iter()
.map(|prop| {
format!(
"{} = {}",
prop.key.name,
prop.value
.recast(options, indentation_level + 1, ExprContext::Other)
.trim()
)
})
.collect::<Vec<String>>()
.join(", "),
);
result.push(')');
result.push('\n');
}
result
}
}
}
}
@ -343,7 +411,7 @@ impl ArrayExpression {
.iter()
.map(|nc| {
found_line_comment |= nc.value.should_cause_array_newline();
nc.format("")
nc.recast(options, 0)
})
.collect::<Vec<_>>()
} else {
@ -481,7 +549,7 @@ impl ObjectExpression {
let format_items: Vec<_> = (0..num_items)
.flat_map(|i| {
if let Some(noncode) = self.non_code_meta.non_code_nodes.get(&i) {
noncode.iter().map(|nc| nc.format("")).collect::<Vec<_>>()
noncode.iter().map(|nc| nc.recast(options, 0)).collect::<Vec<_>>()
} else {
let prop = props.next().unwrap();
// Use a comma unless it's the last item
@ -617,10 +685,13 @@ impl Node<PipeExpression> {
if let Some(non_code_meta_value) = non_code_meta.non_code_nodes.get(&index) {
for val in non_code_meta_value {
let formatted = if val.end == self.end {
let indentation = options.get_indentation(indentation_level);
val.format(&indentation).trim_end_matches('\n').to_string()
val.recast(options, indentation_level)
.trim_end_matches('\n')
.to_string()
} else {
val.format(&indentation).trim_end_matches('\n').to_string()
val.recast(options, indentation_level + 1)
.trim_end_matches('\n')
.to_string()
};
if let NonCodeValue::BlockComment { .. } = val.value {
s += "\n";
@ -1252,7 +1323,8 @@ part001 = startSketchOn('XY')
#[test]
fn test_recast_large_file() {
let some_program_string = r#"// define nts
let some_program_string = r#"@settings(units=mm)
// define nts
radius = 6.0
width = 144.0
length = 83.0
@ -1376,7 +1448,8 @@ tabs_l = startSketchOn({
// Its VERY important this comes back with zero new lines.
assert_eq!(
recasted,
r#"// define nts
r#"@settings(units = mm)
// define nts
radius = 6.0
width = 144.0
length = 83.0