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:
@ -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"),
|
||||
|
@ -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<_>>())
|
||||
|
@ -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],
|
||||
|
@ -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)]
|
||||
|
@ -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(),
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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""#;
|
||||
|
@ -47,10 +47,6 @@ expression: actual
|
||||
"start": 7,
|
||||
"type": "Identifier"
|
||||
},
|
||||
"type_": {
|
||||
"type": "Primitive",
|
||||
"type": "Number"
|
||||
},
|
||||
"default_value": {
|
||||
"type": "Literal",
|
||||
"type": "Literal",
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
Reference in New Issue
Block a user