UoM types in AST, unnamed annotations, and syntax for importing from std (#5324)

Signed-off-by: Nick Cameron <nrc@ncameron.org>
This commit is contained in:
Nick Cameron
2025-02-11 11:26:14 +13:00
committed by GitHub
parent e2be66b024
commit 9c18060d73
12 changed files with 437 additions and 206 deletions

View File

@ -10,6 +10,7 @@ use crate::{
pub(crate) const SETTINGS: &str = "settings";
pub(crate) const SETTINGS_UNIT_LENGTH: &str = "defaultLengthUnit";
pub(crate) const SETTINGS_UNIT_ANGLE: &str = "defaultAngleUnit";
pub(super) const NO_PRELUDE: &str = "no_prelude";
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub(super) enum AnnotationScope {
@ -23,7 +24,7 @@ pub(super) fn expect_properties<'a>(
) -> Result<&'a [Node<ObjectProperty>], KclError> {
match annotation {
NonCodeValue::Annotation { name, properties } => {
assert_eq!(name.name, for_key);
assert_eq!(name.as_ref().unwrap().name, for_key);
Ok(&**properties.as_ref().ok_or_else(|| {
KclError::Semantic(KclErrorDetails {
message: format!("Empty `{for_key}` annotation"),

View File

@ -128,7 +128,7 @@ pub(super) async fn get_changed_program(old: CacheInformation<'_>, new: CacheInf
properties: new_properties,
},
) => {
name.digest == new_name.digest
name.as_ref().map(|n| n.digest) == new_name.as_ref().map(|n| n.digest)
&& properties
.as_ref()
.map(|props| props.iter().map(|p| p.digest).collect::<Vec<_>>())

View File

@ -10,10 +10,9 @@ use crate::{
annotations,
cad_op::{OpArg, Operation},
state::ModuleState,
BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ModuleRepr, ProgramMemory,
TagEngineInfo, TagIdentifier,
BodyType, ExecState, ExecutorContext, KclValue, MemoryFunction, Metadata, ModulePath, ModuleRepr,
ProgramMemory, TagEngineInfo, TagIdentifier,
},
fs::FileSystem,
parsing::ast::types::{
ArrayExpression, ArrayRangeExpression, BinaryExpression, BinaryOperator, BinaryPart, BodyItem, CallExpression,
CallExpressionKw, Expr, FunctionExpression, IfExpression, ImportPath, ImportSelector, ItemVisibility,
@ -38,7 +37,8 @@ impl ExecutorContext {
annotations: impl Iterator<Item = (&NonCodeValue, SourceRange)>,
scope: annotations::AnnotationScope,
exec_state: &mut ExecState,
) -> Result<(), KclError> {
) -> Result<bool, KclError> {
let mut no_prelude = false;
for (annotation, source_range) in annotations {
if annotation.annotation_name() == Some(annotations::SETTINGS) {
if scope == annotations::AnnotationScope::Module {
@ -48,7 +48,7 @@ impl ExecutorContext {
.settings
.update_from_annotation(annotation, source_range)?;
let new_units = exec_state.length_unit();
if old_units != new_units {
if !self.engine.execution_kind().is_isolated() && old_units != new_units {
self.engine.set_units(new_units.into(), source_range).await?;
}
} else {
@ -58,9 +58,19 @@ impl ExecutorContext {
}));
}
}
if annotation.annotation_name() == Some(annotations::NO_PRELUDE) {
if scope == annotations::AnnotationScope::Module {
no_prelude = true;
} else {
return Err(KclError::Semantic(KclErrorDetails {
message: "Prelude can only be skipped at the top level scope of a file".to_owned(),
source_ranges: vec![source_range],
}));
}
}
// TODO warn on unknown annotations
}
Ok(())
Ok(no_prelude)
}
/// Execute an AST's program.
@ -71,22 +81,32 @@ impl ExecutorContext {
exec_state: &mut ExecState,
body_type: BodyType,
) -> Result<Option<KclValue>, KclError> {
self.handle_annotations(
program
.non_code_meta
.start_nodes
.iter()
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))),
annotations::AnnotationScope::Module,
exec_state,
)
.await?;
if body_type == BodyType::Root {
let _no_prelude = self
.handle_annotations(
program
.non_code_meta
.start_nodes
.iter()
.filter_map(|n| n.annotation().map(|result| (result, n.as_source_range()))),
annotations::AnnotationScope::Module,
exec_state,
)
.await?;
}
let mut last_expr = None;
// Iterate over the body of the program.
for statement in &program.body {
for (i, statement) in program.body.iter().enumerate() {
match statement {
BodyItem::ImportStatement(import_stmt) => {
if body_type != BodyType::Root {
return Err(KclError::Semantic(KclErrorDetails {
message: "Imports are only supported at the top-level of a file.".to_owned(),
source_ranges: vec![import_stmt.into()],
}));
}
let source_range = SourceRange::from(import_stmt);
let module_id = self.open_module(&import_stmt.path, exec_state, source_range).await?;
@ -178,6 +198,14 @@ impl ExecutorContext {
let source_range = SourceRange::from(&variable_declaration.declaration.init);
let metadata = Metadata { source_range };
let _meta_nodes = if i == 0 {
&program.non_code_meta.start_nodes
} else if let Some(meta) = program.non_code_meta.non_code_nodes.get(&(i - 1)) {
meta
} else {
&Vec::new()
};
let memory_item = self
.execute_expr(
&variable_declaration.declaration.init,
@ -231,63 +259,45 @@ impl ExecutorContext {
exec_state: &mut ExecState,
source_range: SourceRange,
) -> Result<ModuleId, KclError> {
let resolved_path = ModulePath::from_import_path(path, &self.settings.project_directory);
match path {
ImportPath::Kcl { filename } => {
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
project_dir.join(filename)
} else {
std::path::PathBuf::from(filename)
};
ImportPath::Kcl { .. } => {
exec_state.global.mod_loader.cycle_check(&resolved_path, source_range)?;
if exec_state.mod_local.import_stack.contains(&resolved_path) {
return Err(KclError::ImportCycle(KclErrorDetails {
message: format!(
"circular import of modules is not allowed: {} -> {}",
exec_state
.mod_local
.import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>()
.join(" -> "),
resolved_path.to_string_lossy()
),
source_ranges: vec![source_range],
}));
if let Some(id) = exec_state.id_for_module(&resolved_path) {
return Ok(id);
}
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
return Ok(*id);
}
let source = self.fs.read_to_string(&resolved_path, source_range).await?;
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
let id = exec_state.next_module_id();
let source = resolved_path.source(&self.fs, source_range).await?;
// TODO handle parsing errors properly
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err()?;
let repr = ModuleRepr::Kcl(parsed);
Ok(exec_state.add_module(id, resolved_path, repr))
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed));
Ok(id)
}
ImportPath::Foreign { path } => {
let resolved_path = if let Some(project_dir) = &self.settings.project_directory {
project_dir.join(path)
} else {
std::path::PathBuf::from(path)
};
if let Some(id) = exec_state.global.path_to_source_id.get(&resolved_path) {
return Ok(*id);
ImportPath::Foreign { .. } => {
if let Some(id) = exec_state.id_for_module(&resolved_path) {
return Ok(id);
}
let geom = super::import::import_foreign(&resolved_path, None, exec_state, self, source_range).await?;
let repr = ModuleRepr::Foreign(geom);
let id = ModuleId::from_usize(exec_state.global.path_to_source_id.len());
Ok(exec_state.add_module(id, resolved_path, repr))
let id = exec_state.next_module_id();
let geom =
super::import::import_foreign(resolved_path.expect_path(), None, exec_state, self, source_range)
.await?;
exec_state.add_module(id, resolved_path, ModuleRepr::Foreign(geom));
Ok(id)
}
ImportPath::Std { .. } => {
if let Some(id) = exec_state.id_for_module(&resolved_path) {
return Ok(id);
}
let id = exec_state.next_module_id();
let source = resolved_path.source(&self.fs, source_range).await?;
let parsed = crate::parsing::parse_str(&source, id).parse_errs_as_err().unwrap();
exec_state.add_module(id, resolved_path, ModuleRepr::Kcl(parsed));
Ok(id)
}
i => Err(KclError::Semantic(KclErrorDetails {
message: format!("Unsupported import: `{i}`"),
source_ranges: vec![source_range],
})),
}
}
@ -307,22 +317,20 @@ impl ExecutorContext {
message: format!(
"circular import of modules is not allowed: {} -> {}",
exec_state
.mod_local
.global
.mod_loader
.import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>()
.join(" -> "),
info.path.display()
info.path
),
source_ranges: vec![source_range],
})),
ModuleRepr::Kcl(program) => {
let mut local_state = ModuleState {
import_stack: exec_state.mod_local.import_stack.clone(),
..ModuleState::new(&self.settings)
};
local_state.import_stack.push(info.path.clone());
let mut local_state = ModuleState::new(&self.settings);
exec_state.global.mod_loader.enter_module(&info.path);
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
let original_execution = self.engine.replace_execution_kind(exec_kind);
@ -332,7 +340,8 @@ impl ExecutorContext {
let new_units = exec_state.length_unit();
std::mem::swap(&mut exec_state.mod_local, &mut local_state);
if new_units != old_units {
exec_state.global.mod_loader.leave_module(&info.path);
if !exec_kind.is_isolated() && new_units != old_units {
self.engine.set_units(old_units.into(), Default::default()).await?;
}
self.engine.replace_execution_kind(original_execution);
@ -345,7 +354,7 @@ impl ExecutorContext {
KclError::Semantic(KclErrorDetails {
message: format!(
"Error loading imported file. Open it to view more details. {}: {}",
info.path.display(),
info.path,
err.message()
),
source_ranges: vec![source_range],

View File

@ -1,6 +1,6 @@
//! The executor for the AST.
use std::{path::PathBuf, sync::Arc};
use std::{fmt, path::PathBuf, sync::Arc};
use anyhow::Result;
pub use artifact::{Artifact, ArtifactCommand, ArtifactGraph, ArtifactId};
@ -26,13 +26,13 @@ pub use state::{ExecState, IdGenerator, MetaSettings};
use crate::{
engine::EngineManager,
errors::KclError,
errors::{KclError, KclErrorDetails},
execution::{
artifact::build_artifact_graph,
cache::{CacheInformation, CacheResult},
},
fs::FileManager,
parsing::ast::types::{Expr, FunctionExpression, Node, NodeRef, Program},
fs::{FileManager, FileSystem},
parsing::ast::types::{Expr, FunctionExpression, ImportPath, Node, NodeRef, Program},
settings::types::UnitLength,
source_range::{ModuleId, SourceRange},
std::{args::Arg, StdLib},
@ -169,10 +169,62 @@ pub struct ModuleInfo {
/// The ID of the module.
id: ModuleId,
/// Absolute path of the module's source file.
path: std::path::PathBuf,
path: ModulePath,
repr: ModuleRepr,
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize, Hash)]
pub enum ModulePath {
Local(std::path::PathBuf),
Std(String),
}
impl ModulePath {
fn expect_path(&self) -> &std::path::PathBuf {
match self {
ModulePath::Local(p) => p,
_ => unreachable!(),
}
}
pub(crate) async fn source(&self, fs: &FileManager, source_range: SourceRange) -> Result<String, KclError> {
match self {
ModulePath::Local(p) => fs.read_to_string(p, source_range).await,
ModulePath::Std(_) => unimplemented!(),
}
}
pub(crate) fn from_import_path(path: &ImportPath, project_directory: &Option<PathBuf>) -> Self {
match path {
ImportPath::Kcl { filename: path } | ImportPath::Foreign { path } => {
let resolved_path = if let Some(project_dir) = project_directory {
project_dir.join(path)
} else {
std::path::PathBuf::from(path)
};
ModulePath::Local(resolved_path)
}
ImportPath::Std { path } => {
// For now we only support importing from singly-nested modules inside std.
assert_eq!(path.len(), 2);
assert_eq!(&path[0], "std");
ModulePath::Std(path[1].clone())
}
}
}
}
impl fmt::Display for ModulePath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ModulePath::Local(path) => path.display().fmt(f),
ModulePath::Std(s) => write!(f, "std::{s}"),
}
}
}
#[allow(clippy::large_enum_variant)]
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
pub enum ModuleRepr {
@ -181,6 +233,47 @@ pub enum ModuleRepr {
Foreign(import::PreImportedGeometry),
}
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct ModuleLoader {
/// The stack of import statements for detecting circular module imports.
/// If this is empty, we're not currently executing an import statement.
pub import_stack: Vec<PathBuf>,
}
impl ModuleLoader {
pub(crate) fn cycle_check(&self, path: &ModulePath, source_range: SourceRange) -> Result<(), KclError> {
if self.import_stack.contains(path.expect_path()) {
return Err(KclError::ImportCycle(KclErrorDetails {
message: format!(
"circular import of modules is not allowed: {} -> {}",
self.import_stack
.iter()
.map(|p| p.as_path().to_string_lossy())
.collect::<Vec<_>>()
.join(" -> "),
path,
),
source_ranges: vec![source_range],
}));
}
Ok(())
}
pub(crate) fn enter_module(&mut self, path: &ModulePath) {
if let ModulePath::Local(ref path) = path {
self.import_stack.push(path.clone());
}
}
pub(crate) fn leave_module(&mut self, path: &ModulePath) {
if let ModulePath::Local(ref path) = path {
let popped = self.import_stack.pop().unwrap();
assert_eq!(path, &popped);
}
}
}
/// Metadata.
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema, Eq, Copy)]
#[ts(export)]

View File

@ -9,7 +9,8 @@ use crate::{
errors::{KclError, KclErrorDetails},
execution::{
annotations, kcl_value, Artifact, ArtifactCommand, ArtifactGraph, ArtifactId, ExecOutcome, ExecutorSettings,
KclValue, ModuleInfo, ModuleRepr, Operation, ProgramMemory, SolidLazyIds, UnitAngle, UnitLen,
KclValue, ModuleInfo, ModuleLoader, ModulePath, ModuleRepr, Operation, ProgramMemory, SolidLazyIds, UnitAngle,
UnitLen,
},
parsing::ast::types::NonCodeValue,
source_range::{ModuleId, SourceRange},
@ -29,7 +30,7 @@ pub struct GlobalState {
/// The stable artifact ID generator.
pub id_generator: IdGenerator,
/// Map from source file absolute path to module ID.
pub path_to_source_id: IndexMap<std::path::PathBuf, ModuleId>,
pub path_to_source_id: IndexMap<ModulePath, ModuleId>,
/// Map from module ID to module info.
pub module_infos: IndexMap<ModuleId, ModuleInfo>,
/// Output map of UUIDs to artifacts.
@ -45,6 +46,8 @@ pub struct GlobalState {
pub artifact_responses: IndexMap<Uuid, WebSocketResponse>,
/// Output artifact graph.
pub artifact_graph: ArtifactGraph,
/// Module loader.
pub mod_loader: ModuleLoader,
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
@ -59,9 +62,6 @@ pub struct ModuleState {
pub pipe_value: Option<KclValue>,
/// Identifiers that have been exported from the current module.
pub module_exports: Vec<String>,
/// The stack of import statements for detecting circular module imports.
/// If this is empty, we're not currently executing an import statement.
pub import_stack: Vec<std::path::PathBuf>,
/// Operations that have been performed in execution order, for display in
/// the Feature Tree.
pub operations: Vec<Operation>,
@ -124,15 +124,21 @@ impl ExecState {
self.global.artifacts.insert(id, artifact);
}
pub(super) fn add_module(&mut self, id: ModuleId, path: std::path::PathBuf, repr: ModuleRepr) -> ModuleId {
pub(super) fn next_module_id(&self) -> ModuleId {
ModuleId::from_usize(self.global.path_to_source_id.len())
}
pub(super) fn id_for_module(&self, path: &ModulePath) -> Option<ModuleId> {
self.global.path_to_source_id.get(path).cloned()
}
pub(super) fn add_module(&mut self, id: ModuleId, path: ModulePath, repr: ModuleRepr) {
debug_assert!(!self.global.path_to_source_id.contains_key(&path));
self.global.path_to_source_id.insert(path.clone(), id);
let module_info = ModuleInfo { id, repr, path };
self.global.module_infos.insert(id, module_info);
id
}
pub fn length_unit(&self) -> UnitLen {
@ -154,6 +160,7 @@ impl GlobalState {
artifact_commands: Default::default(),
artifact_responses: Default::default(),
artifact_graph: Default::default(),
mod_loader: Default::default(),
};
let root_id = ModuleId::default();
@ -162,11 +169,11 @@ impl GlobalState {
root_id,
ModuleInfo {
id: root_id,
path: root_path.clone(),
path: ModulePath::Local(root_path.clone()),
repr: ModuleRepr::Root,
},
);
global.path_to_source_id.insert(root_path, root_id);
global.path_to_source_id.insert(ModulePath::Local(root_path), root_id);
global
}
}
@ -178,7 +185,6 @@ impl ModuleState {
dynamic_state: Default::default(),
pipe_value: Default::default(),
module_exports: Default::default(),
import_stack: Default::default(),
operations: Default::default(),
settings: MetaSettings {
default_length_units: exec_settings.units.into(),

View File

@ -121,7 +121,9 @@ impl NonCodeValue {
ref mut name,
properties,
} => {
hasher.update(name.compute_digest());
if let Some(name) = name {
hasher.update(name.compute_digest());
}
if let Some(properties) = properties {
hasher.update(properties.len().to_ne_bytes());
for property in properties.iter_mut() {

View File

@ -27,6 +27,7 @@ use crate::{
errors::KclError,
execution::{annotations, KclValue, Metadata, TagIdentifier},
parsing::{ast::digest::Digest, PIPE_OPERATOR},
pretty::NumericSuffix,
source_range::{ModuleId, SourceRange},
};
@ -868,6 +869,43 @@ impl Expr {
}
}
pub fn literal_bool(&self) -> Option<bool> {
match self {
Expr::Literal(lit) => match lit.value {
LiteralValue::Bool(b) => Some(b),
_ => None,
},
_ => None,
}
}
pub fn literal_num(&self) -> Option<(f64, NumericSuffix)> {
match self {
Expr::Literal(lit) => match lit.value {
LiteralValue::Number { value, suffix } => Some((value, suffix)),
_ => None,
},
_ => None,
}
}
pub fn literal_str(&self) -> Option<&str> {
match self {
Expr::Literal(lit) => match &lit.value {
LiteralValue::String(s) => Some(s),
_ => None,
},
_ => None,
}
}
pub fn ident_name(&self) -> Option<&str> {
match self {
Expr::Identifier(ident) => Some(&ident.name),
_ => None,
}
}
/// Describe this expression's type for a human, for typechecking.
/// This is a best-effort function, it's OK to give a shitty string here (but we should work on improving it)
pub fn human_friendly_type(&self) -> &'static str {
@ -1089,7 +1127,7 @@ impl NonCodeNode {
NonCodeValue::BlockComment { value, style: _ } => value.clone(),
NonCodeValue::NewLineBlockComment { value, style: _ } => value.clone(),
NonCodeValue::NewLine => "\n\n".to_string(),
NonCodeValue::Annotation { name, .. } => name.name.clone(),
n @ NonCodeValue::Annotation { .. } => n.annotation_name().unwrap_or("").to_owned(),
}
}
@ -1158,7 +1196,7 @@ pub enum NonCodeValue {
// This is also not a comment.
NewLine,
Annotation {
name: Node<Identifier>,
name: Option<Node<Identifier>>,
properties: Option<Vec<Node<ObjectProperty>>>,
},
}
@ -1166,7 +1204,7 @@ pub enum NonCodeValue {
impl NonCodeValue {
pub fn annotation_name(&self) -> Option<&str> {
match self {
NonCodeValue::Annotation { name, .. } => Some(&name.name),
NonCodeValue::Annotation { name, .. } => name.as_ref().map(|i| &*i.name),
_ => None,
}
}
@ -1184,7 +1222,7 @@ impl NonCodeValue {
));
}
NonCodeValue::Annotation {
name: Identifier::new(annotations::SETTINGS),
name: Some(Identifier::new(annotations::SETTINGS)),
properties: Some(properties),
}
}
@ -1212,6 +1250,20 @@ impl NonCodeMeta {
pub fn non_code_nodes_len(&self) -> usize {
self.non_code_nodes.values().map(|x| x.len()).sum()
}
pub fn insert(&mut self, i: usize, new: Node<NonCodeNode>) {
self.non_code_nodes.entry(i).or_default().push(new);
}
pub fn contains(&self, pos: usize) -> bool {
if self.start_nodes.iter().any(|node| node.contains(pos)) {
return true;
}
self.non_code_nodes
.iter()
.any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos)))
}
}
// implement Deserialize manually because we to force the keys of non_code_nodes to be usize
@ -1242,22 +1294,6 @@ impl<'de> Deserialize<'de> for NonCodeMeta {
}
}
impl NonCodeMeta {
pub fn insert(&mut self, i: usize, new: Node<NonCodeNode>) {
self.non_code_nodes.entry(i).or_default().push(new);
}
pub fn contains(&self, pos: usize) -> bool {
if self.start_nodes.iter().any(|node| node.contains(pos)) {
return true;
}
self.non_code_nodes
.iter()
.any(|(_, nodes)| nodes.iter().any(|node| node.contains(pos)))
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, ts_rs::TS, JsonSchema)]
#[ts(export)]
#[serde(tag = "type")]
@ -1382,14 +1418,14 @@ impl ImportSelector {
pub enum ImportPath {
Kcl { filename: String },
Foreign { path: String },
Std,
Std { path: Vec<String> },
}
impl fmt::Display for ImportPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ImportPath::Kcl { filename: s } | ImportPath::Foreign { path: s } => write!(f, "{s}"),
ImportPath::Std => write!(f, "std"),
ImportPath::Std { path } => write!(f, "{}", path.join("::")),
}
}
}
@ -1746,7 +1782,7 @@ pub enum ItemVisibility {
}
impl ItemVisibility {
fn is_default(&self) -> bool {
pub fn is_default(&self) -> bool {
matches!(self, Self::Default)
}
}
@ -2941,16 +2977,14 @@ impl PipeExpression {
}
}
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema, FromStr, Display)]
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq, JsonSchema)]
#[serde(tag = "type")]
#[display(style = "snake_case")]
pub enum FnArgPrimitive {
/// A string type.
String,
/// A number type.
Number,
Number(NumericSuffix),
/// A boolean type.
#[display("bool")]
#[serde(rename = "bool")]
Boolean,
/// A tag.
@ -2967,12 +3001,46 @@ impl FnArgPrimitive {
pub fn digestable_id(&self) -> &[u8] {
match self {
FnArgPrimitive::String => b"string",
FnArgPrimitive::Number => b"number",
FnArgPrimitive::Boolean => b"boolean",
FnArgPrimitive::Number(suffix) => suffix.digestable_id(),
FnArgPrimitive::Boolean => b"bool",
FnArgPrimitive::Tag => b"tag",
FnArgPrimitive::Sketch => b"sketch",
FnArgPrimitive::SketchSurface => b"sketch_surface",
FnArgPrimitive::Solid => b"solid",
FnArgPrimitive::Sketch => b"Sketch",
FnArgPrimitive::SketchSurface => b"SketchSurface",
FnArgPrimitive::Solid => b"Solid",
}
}
pub fn from_str(s: &str, suffix: Option<NumericSuffix>) -> Option<Self> {
match (s, suffix) {
("string", None) => Some(FnArgPrimitive::String),
("bool", None) => Some(FnArgPrimitive::Boolean),
("tag", None) => Some(FnArgPrimitive::Tag),
("Sketch", None) => Some(FnArgPrimitive::Sketch),
("SketchSurface", None) => Some(FnArgPrimitive::SketchSurface),
("Solid", None) => Some(FnArgPrimitive::Solid),
("number", None) => Some(FnArgPrimitive::Number(NumericSuffix::None)),
("number", Some(s)) => Some(FnArgPrimitive::Number(s)),
_ => None,
}
}
}
impl fmt::Display for FnArgPrimitive {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FnArgPrimitive::Number(suffix) => {
write!(f, "number")?;
if *suffix != NumericSuffix::None {
write!(f, "({suffix})")?;
}
Ok(())
}
FnArgPrimitive::String => write!(f, "string"),
FnArgPrimitive::Boolean => write!(f, "bool"),
FnArgPrimitive::Tag => write!(f, "tag"),
FnArgPrimitive::Sketch => write!(f, "Sketch"),
FnArgPrimitive::SketchSurface => write!(f, "SketchSurface"),
FnArgPrimitive::Solid => write!(f, "Solid"),
}
}
}
@ -3029,8 +3097,7 @@ pub struct Parameter {
pub identifier: Node<Identifier>,
/// The type of the parameter.
/// This is optional if the user defines a type.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[ts(skip)]
#[serde(skip)]
pub type_: Option<FnArgType>,
/// Is the parameter optional?
/// If so, what is its default value?
@ -3516,7 +3583,7 @@ const cylinder = startSketchOn('-XZ')
#[tokio::test(flavor = "multi_thread")]
async fn test_parse_type_args_on_functions() {
let some_program_string = r#"fn thing = (arg0: number, arg1: string, tag?: string) => {
let some_program_string = r#"fn thing = (arg0: number(mm), arg1: string, tag?: string) => {
return arg0
}"#;
let program = crate::parsing::top_level_parse(some_program_string).unwrap();
@ -3531,7 +3598,10 @@ const cylinder = startSketchOn('-XZ')
};
let params = &func_expr.params;
assert_eq!(params.len(), 3);
assert_eq!(params[0].type_, Some(FnArgType::Primitive(FnArgPrimitive::Number)));
assert_eq!(
params[0].type_,
Some(FnArgType::Primitive(FnArgPrimitive::Number(NumericSuffix::Mm)))
);
assert_eq!(params[1].type_, Some(FnArgType::Primitive(FnArgPrimitive::String)));
assert_eq!(params[2].type_, Some(FnArgType::Primitive(FnArgPrimitive::String)));
}
@ -3553,7 +3623,10 @@ const cylinder = startSketchOn('-XZ')
};
let params = &func_expr.params;
assert_eq!(params.len(), 3);
assert_eq!(params[0].type_, Some(FnArgType::Array(FnArgPrimitive::Number)));
assert_eq!(
params[0].type_,
Some(FnArgType::Array(FnArgPrimitive::Number(NumericSuffix::None)))
);
assert_eq!(params[1].type_, Some(FnArgType::Array(FnArgPrimitive::String)));
assert_eq!(params[2].type_, Some(FnArgType::Primitive(FnArgPrimitive::String)));
}
@ -3576,7 +3649,10 @@ const cylinder = startSketchOn('-XZ')
};
let params = &func_expr.params;
assert_eq!(params.len(), 3);
assert_eq!(params[0].type_, Some(FnArgType::Array(FnArgPrimitive::Number)));
assert_eq!(
params[0].type_,
Some(FnArgType::Array(FnArgPrimitive::Number(NumericSuffix::None)))
);
assert_eq!(
params[1].type_,
Some(FnArgType::Object {
@ -3591,7 +3667,7 @@ const cylinder = startSketchOn('-XZ')
40,
module_id,
),
type_: Some(FnArgType::Primitive(FnArgPrimitive::Number)),
type_: Some(FnArgType::Primitive(FnArgPrimitive::Number(NumericSuffix::None))),
default_value: None,
labeled: true,
digest: None,
@ -3664,7 +3740,7 @@ const cylinder = startSketchOn('-XZ')
18,
module_id,
),
type_: Some(FnArgType::Primitive(FnArgPrimitive::Number)),
type_: Some(FnArgType::Primitive(FnArgPrimitive::Number(NumericSuffix::None))),
default_value: None,
labeled: true,
digest: None

View File

@ -1,7 +1,7 @@
// TODO optimise size of CompilationError
#![allow(clippy::result_large_err)]
use std::{cell::RefCell, collections::BTreeMap, str::FromStr};
use std::{cell::RefCell, collections::BTreeMap};
use winnow::{
combinator::{alt, delimited, opt, peek, preceded, repeat, separated, separated_pair, terminated},
@ -286,8 +286,8 @@ fn non_code_node(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
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 name = opt(binding_name).parse_next(i)?;
let mut end = name.as_ref().map(|n| n.end).unwrap_or(at.end);
let properties = if peek(open_paren).parse_next(i).is_ok() {
open_paren(i)?;
@ -320,6 +320,12 @@ fn annotation(i: &mut TokenSlice) -> PResult<Node<NonCodeNode>> {
None
};
if name.is_none() && properties.is_none() {
return Err(ErrMode::Cut(
CompilationError::fatal(at.as_source_range(), format!("Unexpected token: {}", at.value)).into(),
));
}
let value = NonCodeValue::Annotation { name, properties };
Ok(Node::new(
NonCodeNode { value, digest: None },
@ -491,7 +497,7 @@ pub(crate) fn unsigned_number_literal(i: &mut TokenSlice) -> PResult<Node<Litera
})?;
if token.numeric_suffix().is_some() {
ParseContext::err(CompilationError::err(
ParseContext::warn(CompilationError::err(
(&token).into(),
"Unit of Measure suffixes are experimental and currently do nothing.",
));
@ -1117,9 +1123,25 @@ fn function_decl(i: &mut TokenSlice) -> PResult<(Node<FunctionExpression>, bool)
// Optional return type.
let return_type = opt(return_type).parse_next(i)?;
ignore_whitespace(i);
open_brace(i)?;
let body = function_body(i)?;
let end = close_brace(i)?.end;
let brace = open_brace(i)?;
let close: Option<(Vec<Vec<Token>>, Token)> = opt((repeat(0.., whitespace), close_brace)).parse_next(i)?;
let (body, end) = match close {
Some((_, end)) => (
Node::new(
Program {
body: Vec::new(),
non_code_meta: NonCodeMeta::default(),
shebang: None,
digest: None,
},
brace.end,
brace.end,
brace.module_id,
),
end.end,
),
None => (function_body(i)?, close_brace(i)?.end),
};
let result = Node::new(
FunctionExpression {
params,
@ -1587,6 +1609,14 @@ fn import_stmt(i: &mut TokenSlice) -> PResult<BoxNode<ImportStatement>> {
)
.into(),
));
} else if matches!(path, ImportPath::Std { .. }) && matches!(selector, ImportSelector::None { .. }) {
return Err(ErrMode::Cut(
CompilationError::fatal(
SourceRange::new(start, end, module_id),
"the standard library cannot be imported as a part",
)
.into(),
));
}
Ok(Node::boxed(
@ -1639,13 +1669,34 @@ fn validate_path_string(path_string: String, var_name: bool, path_range: SourceR
}
ImportPath::Kcl { filename: path_string }
} else if path_string.starts_with("std") {
} else if path_string.starts_with("std::") {
ParseContext::warn(CompilationError::err(
path_range,
"explicit imports from the standard library are experimental, likely to be buggy, and likely to change.",
));
ImportPath::Std
let segments: Vec<String> = path_string.split("::").map(str::to_owned).collect();
for s in &segments {
if s.chars().any(|c| !c.is_ascii_alphanumeric() && c != '_') || s.starts_with('_') {
return Err(ErrMode::Cut(
CompilationError::fatal(path_range, "invalid path in import statement.").into(),
));
}
}
// For now we only support importing from singly-nested modules inside std.
if segments.len() != 2 {
return Err(ErrMode::Cut(
CompilationError::fatal(
path_range,
format!("Invalid import path for import from std: {}.", path_string),
)
.into(),
));
}
ImportPath::Std { path: segments }
} else if path_string.contains('.') {
let extn = &path_string[path_string.rfind('.').unwrap() + 1..];
if !FOREIGN_IMPORT_EXTENSIONS.contains(&extn) {
@ -2433,11 +2484,19 @@ fn argument_type(i: &mut TokenSlice) -> PResult<FnArgType> {
// TODO it is buggy to treat object fields like parameters since the parameters parser assumes a terminating `)`.
(open_brace, parameters, close_brace).map(|(_, params, _)| Ok(FnArgType::Object { properties: params })),
// Array types
(one_of(TokenType::Type), open_bracket, close_bracket).map(|(token, _, _)| {
FnArgPrimitive::from_str(&token.value)
.map(FnArgType::Array)
.map_err(|err| CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", err)))
}),
(
one_of(TokenType::Type),
opt(delimited(open_paren, uom_for_type, close_paren)),
open_bracket,
close_bracket,
)
.map(|(token, uom, _, _)| {
FnArgPrimitive::from_str(&token.value, uom)
.map(FnArgType::Array)
.ok_or_else(|| {
CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", token.value))
})
}),
// Primitive types
(
one_of(TokenType::Type),
@ -2445,14 +2504,16 @@ fn argument_type(i: &mut TokenSlice) -> PResult<FnArgType> {
)
.map(|(token, suffix)| {
if suffix.is_some() {
ParseContext::err(CompilationError::err(
ParseContext::warn(CompilationError::err(
(&token).into(),
"Unit of Measure types are experimental and currently do nothing.",
));
}
FnArgPrimitive::from_str(&token.value)
FnArgPrimitive::from_str(&token.value, suffix)
.map(FnArgType::Primitive)
.map_err(|err| CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", err)))
.ok_or_else(|| {
CompilationError::fatal(token.as_source_range(), format!("Invalid type: {}", token.value))
})
}),
))
.parse_next(i)?
@ -2516,8 +2577,7 @@ fn parameters(i: &mut TokenSlice) -> PResult<Vec<Parameter>> {
type_,
default_value,
}| {
let identifier =
Node::<Identifier>::try_from(arg_name).and_then(Node::<Identifier>::into_valid_binding_name)?;
let identifier = Node::<Identifier>::try_from(arg_name)?;
Ok(Parameter {
identifier,
@ -2564,24 +2624,9 @@ fn optional_after_required(params: &[Parameter]) -> Result<(), CompilationError>
Ok(())
}
impl Node<Identifier> {
fn into_valid_binding_name(self) -> Result<Node<Identifier>, CompilationError> {
// Make sure they are not assigning a variable to a stdlib function.
if crate::std::name_in_stdlib(&self.name) {
return Err(CompilationError::fatal(
SourceRange::from(&self),
format!("Cannot assign a variable to a reserved keyword: {}", self.name),
));
}
Ok(self)
}
}
/// Introduce a new name, which binds some value.
fn binding_name(i: &mut TokenSlice) -> PResult<Node<Identifier>> {
identifier
.context(expected("an identifier, which will be the name of some value"))
.try_map(Node::<Identifier>::into_valid_binding_name)
.context(expected("an identifier, which will be the name of some value"))
.parse_next(i)
}
@ -4018,17 +4063,6 @@ e
);
}
#[test]
fn test_error_stdlib_in_fn_name() {
assert_err(
r#"fn cos = () => {
return 1
}"#,
"Cannot assign a variable to a reserved keyword: cos",
[3, 6],
);
}
#[test]
fn test_error_keyword_in_fn_args() {
assert_err(
@ -4040,17 +4074,6 @@ e
)
}
#[test]
fn test_error_stdlib_in_fn_args() {
assert_err(
r#"fn thing = (cos) => {
return 1
}"#,
"Cannot assign a variable to a reserved keyword: cos",
[12, 15],
)
}
#[test]
fn bad_imports() {
assert_err(
@ -4095,6 +4118,27 @@ e
);
}
#[test]
fn std_fn_decl() {
let code = r#"/// Compute the cosine of a number (in radians).
///
/// ```
/// exampleSketch = startSketchOn("XZ")
/// |> startProfileAt([0, 0], %)
/// |> angledLine({
/// angle = 30,
/// length = 3 / cos(toRadians(30)),
/// }, %)
/// |> yLineTo(0, %)
/// |> close(%)
///
/// example = extrude(5, exampleSketch)
/// ```
@(impl = std_rust)
export fn cos(num: number(rad)): number(_) {}"#;
let _ast = crate::parsing::top_level_parse(code).unwrap();
}
#[test]
fn warn_import() {
let some_program_string = r#"import "foo.kcl""#;

View File

@ -47,10 +47,6 @@ expression: actual
"start": 7,
"type": "Identifier"
},
"type_": {
"type": "Primitive",
"type": "Number"
},
"default_value": {
"type": "Literal",
"type": "Literal",

View File

@ -54,6 +54,21 @@ impl NumericSuffix {
pub fn is_some(self) -> bool {
self != Self::None
}
pub fn digestable_id(&self) -> &[u8] {
match self {
NumericSuffix::None => &[],
NumericSuffix::Count => b"_",
NumericSuffix::Mm => b"mm",
NumericSuffix::Cm => b"cm",
NumericSuffix::M => b"m",
NumericSuffix::Inch => b"in",
NumericSuffix::Ft => b"ft",
NumericSuffix::Yd => b"yd",
NumericSuffix::Deg => b"deg",
NumericSuffix::Rad => b"rad",
}
}
}
impl FromStr for NumericSuffix {

View File

@ -161,7 +161,9 @@ impl Node<NonCodeNode> {
NonCodeValue::NewLine => "\n\n".to_string(),
NonCodeValue::Annotation { name, properties } => {
let mut result = "@".to_owned();
result.push_str(&name.name);
if let Some(name) = name {
result.push_str(&name.name);
}
if let Some(properties) = properties {
result.push('(');
result.push_str(

View File

@ -1,7 +1,6 @@
---
source: kcl/src/simulation_tests.rs
description: Artifact commands import_function_not_sketch.kcl
snapshot_kind: text
---
[
{
@ -817,17 +816,5 @@ snapshot_kind: text
"edge_id": "[uuid]",
"face_id": "[uuid]"
}
},
{
"cmdId": "[uuid]",
"range": [
0,
0,
0
],
"command": {
"type": "set_scene_units",
"unit": "in"
}
}
]