Compare commits

...

4 Commits

Author SHA1 Message Date
078ffa02b0 Add the at token 2024-11-26 16:19:00 -06:00
d461b09a4d KCL refactor: break typechecking into its own fn (#4587) 2024-11-26 16:02:31 -06:00
9c42c39ba9 Fix publish path in release bucket (#4585) 2024-11-26 15:11:26 -05:00
aa3f40e22c KCL: Two tiny refactors (#4580)
* Refactor: Combine two impl blocks

* Refactor: Constant for NO_META(data)
2024-11-26 12:27:09 -06:00
6 changed files with 150 additions and 135 deletions

View File

@ -123,7 +123,7 @@ jobs:
path: out
glob: '*'
parent: false
destination: 'dl.kittycad.io/releases/modeling-app/test/new-workflow'
destination: 'dl.kittycad.io/releases/modeling-app'
- name: Invalidate bucket cache on latest*.yml and last_download.json files
run: |

View File

@ -197,24 +197,17 @@ pub struct Environment {
parent: Option<EnvironmentRef>,
}
const NO_META: Vec<Metadata> = Vec::new();
impl Environment {
pub fn root() -> Self {
Self {
// Prelude
bindings: HashMap::from([
("ZERO".to_string(), KclValue::from_number(0.0, Default::default())),
(
"QUARTER_TURN".to_string(),
KclValue::from_number(90.0, Default::default()),
),
(
"HALF_TURN".to_string(),
KclValue::from_number(180.0, Default::default()),
),
(
"THREE_QUARTER_TURN".to_string(),
KclValue::from_number(270.0, Default::default()),
),
("ZERO".to_string(), KclValue::from_number(0.0, NO_META)),
("QUARTER_TURN".to_string(), KclValue::from_number(90.0, NO_META)),
("HALF_TURN".to_string(), KclValue::from_number(180.0, NO_META)),
("THREE_QUARTER_TURN".to_string(), KclValue::from_number(270.0, NO_META)),
]),
parent: None,
}

View File

@ -86,83 +86,6 @@ pub enum KclValue {
},
}
impl KclValue {
pub(crate) fn metadata(&self) -> Vec<Metadata> {
match self {
KclValue::Uuid { value: _, meta } => meta.clone(),
KclValue::Bool { value: _, meta } => meta.clone(),
KclValue::Number { value: _, meta } => meta.clone(),
KclValue::Int { value: _, meta } => meta.clone(),
KclValue::String { value: _, meta } => meta.clone(),
KclValue::Array { value: _, meta } => meta.clone(),
KclValue::Object { value: _, meta } => meta.clone(),
KclValue::TagIdentifier(x) => x.meta.clone(),
KclValue::TagDeclarator(x) => vec![x.metadata()],
KclValue::Plane(x) => x.meta.clone(),
KclValue::Face(x) => x.meta.clone(),
KclValue::Sketch { value } => value.meta.clone(),
KclValue::Sketches { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::Solid(x) => x.meta.clone(),
KclValue::Solids { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::ImportedGeometry(x) => x.meta.clone(),
KclValue::Function { meta, .. } => meta.clone(),
KclValue::KclNone { meta, .. } => meta.clone(),
}
}
pub(crate) fn get_solid_set(&self) -> Result<SolidSet> {
match self {
KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())),
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
KclValue::Array { value, .. } => {
let solids: Vec<_> = value
.iter()
.enumerate()
.map(|(i, v)| {
v.as_solid().map(|v| v.to_owned()).map(Box::new).ok_or_else(|| {
anyhow::anyhow!(
"expected this array to only contain solids, but element {i} was actually {}",
v.human_friendly_type()
)
})
})
.collect::<Result<_, _>>()?;
Ok(SolidSet::Solids(solids))
}
_ => anyhow::bail!("Not a solid or solids: {:?}", self),
}
}
/// Human readable type name used in error messages. Should not be relied
/// on for program logic.
pub(crate) fn human_friendly_type(&self) -> &'static str {
match self {
KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid(_) => "Solid",
KclValue::Solids { .. } => "Solids",
KclValue::Sketch { .. } => "Sketch",
KclValue::Sketches { .. } => "Sketches",
KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function",
KclValue::Plane(_) => "Plane",
KclValue::Face(_) => "Face",
KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Number { .. } => "number",
KclValue::Int { .. } => "integer",
KclValue::String { .. } => "string (text)",
KclValue::Array { .. } => "array (list)",
KclValue::Object { .. } => "object",
KclValue::KclNone { .. } => "None",
}
}
pub(crate) fn is_function(&self) -> bool {
matches!(self, KclValue::Function { .. })
}
}
impl From<SketchSet> for KclValue {
fn from(sg: SketchSet) -> Self {
match sg {
@ -251,8 +174,82 @@ impl From<&KclValue> for Vec<SourceRange> {
}
impl KclValue {
pub(crate) fn metadata(&self) -> Vec<Metadata> {
match self {
KclValue::Uuid { value: _, meta } => meta.clone(),
KclValue::Bool { value: _, meta } => meta.clone(),
KclValue::Number { value: _, meta } => meta.clone(),
KclValue::Int { value: _, meta } => meta.clone(),
KclValue::String { value: _, meta } => meta.clone(),
KclValue::Array { value: _, meta } => meta.clone(),
KclValue::Object { value: _, meta } => meta.clone(),
KclValue::TagIdentifier(x) => x.meta.clone(),
KclValue::TagDeclarator(x) => vec![x.metadata()],
KclValue::Plane(x) => x.meta.clone(),
KclValue::Face(x) => x.meta.clone(),
KclValue::Sketch { value } => value.meta.clone(),
KclValue::Sketches { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::Solid(x) => x.meta.clone(),
KclValue::Solids { value } => value.iter().flat_map(|sketch| &sketch.meta).copied().collect(),
KclValue::ImportedGeometry(x) => x.meta.clone(),
KclValue::Function { meta, .. } => meta.clone(),
KclValue::KclNone { meta, .. } => meta.clone(),
}
}
pub(crate) fn get_solid_set(&self) -> Result<SolidSet> {
match self {
KclValue::Solid(e) => Ok(SolidSet::Solid(e.clone())),
KclValue::Solids { value } => Ok(SolidSet::Solids(value.clone())),
KclValue::Array { value, .. } => {
let solids: Vec<_> = value
.iter()
.enumerate()
.map(|(i, v)| {
v.as_solid().map(|v| v.to_owned()).map(Box::new).ok_or_else(|| {
anyhow::anyhow!(
"expected this array to only contain solids, but element {i} was actually {}",
v.human_friendly_type()
)
})
})
.collect::<Result<_, _>>()?;
Ok(SolidSet::Solids(solids))
}
_ => anyhow::bail!("Not a solid or solids: {:?}", self),
}
}
/// Human readable type name used in error messages. Should not be relied
/// on for program logic.
pub(crate) fn human_friendly_type(&self) -> &'static str {
match self {
KclValue::Uuid { .. } => "Unique ID (uuid)",
KclValue::TagDeclarator(_) => "TagDeclarator",
KclValue::TagIdentifier(_) => "TagIdentifier",
KclValue::Solid(_) => "Solid",
KclValue::Solids { .. } => "Solids",
KclValue::Sketch { .. } => "Sketch",
KclValue::Sketches { .. } => "Sketches",
KclValue::ImportedGeometry(_) => "ImportedGeometry",
KclValue::Function { .. } => "Function",
KclValue::Plane(_) => "Plane",
KclValue::Face(_) => "Face",
KclValue::Bool { .. } => "boolean (true/false value)",
KclValue::Number { .. } => "number",
KclValue::Int { .. } => "integer",
KclValue::String { .. } => "string (text)",
KclValue::Array { .. } => "array (list)",
KclValue::Object { .. } => "object",
KclValue::KclNone { .. } => "None",
}
}
pub(crate) fn is_function(&self) -> bool {
matches!(self, KclValue::Function { .. })
}
/// Put the number into a KCL value.
pub fn from_number(f: f64, meta: Vec<Metadata>) -> Self {
pub const fn from_number(f: f64, meta: Vec<Metadata>) -> Self {
Self::Number { value: f, meta }
}

View File

@ -18,6 +18,7 @@ use crate::{
ObjectProperty, Parameter, PipeExpression, PipeSubstitution, Program, ReturnStatement, Shebang, TagDeclarator,
UnaryExpression, UnaryOperator, VariableDeclaration, VariableDeclarator, VariableKind,
},
docs::StdLibFn,
errors::{KclError, KclErrorDetails},
executor::SourceRange,
parser::{
@ -2091,55 +2092,65 @@ fn binding_name(i: TokenSlice) -> PResult<Node<Identifier>> {
.parse_next(i)
}
fn typecheck_all(std_fn: Box<dyn StdLibFn>, args: &[Expr]) -> PResult<()> {
// Type check the arguments.
for (i, spec_arg) in std_fn.args(false).iter().enumerate() {
let Some(arg) = &args.get(i) else {
// The executor checks the number of arguments, so we don't need to check it here.
continue;
};
typecheck(spec_arg, arg)?;
}
Ok(())
}
fn typecheck(spec_arg: &crate::docs::StdLibFnArg, arg: &&Expr) -> PResult<()> {
match spec_arg.type_.as_ref() {
"TagNode" => match &arg {
Expr::Identifier(_) => {
// These are fine since we want someone to be able to map a variable to a tag declarator.
}
Expr::TagDeclarator(tag) => {
// TODO: Remove this check. It should be redundant.
tag.clone()
.into_valid_binding_name()
.map_err(|e| ErrMode::Cut(ContextError::from(e)))?;
}
e => {
return Err(ErrMode::Cut(
KclError::Syntax(KclErrorDetails {
source_ranges: vec![SourceRange::from(*arg)],
message: format!("Expected a tag declarator like `$name`, found {:?}", e),
})
.into(),
));
}
},
"TagIdentifier" => match &arg {
Expr::Identifier(_) => {}
Expr::MemberExpression(_) => {}
e => {
return Err(ErrMode::Cut(
KclError::Syntax(KclErrorDetails {
source_ranges: vec![SourceRange::from(*arg)],
message: format!("Expected a tag identifier like `tagName`, found {:?}", e),
})
.into(),
));
}
},
_ => {}
}
Ok(())
}
fn fn_call(i: TokenSlice) -> PResult<Node<CallExpression>> {
let fn_name = identifier(i)?;
opt(whitespace).parse_next(i)?;
let _ = terminated(open_paren, opt(whitespace)).parse_next(i)?;
let args = arguments(i)?;
if let Some(std_fn) = crate::std::get_stdlib_fn(&fn_name.name) {
// Type check the arguments.
for (i, spec_arg) in std_fn.args(false).iter().enumerate() {
let Some(arg) = &args.get(i) else {
// The executor checks the number of arguments, so we don't need to check it here.
continue;
};
match spec_arg.type_.as_ref() {
"TagNode" => match &arg {
Expr::Identifier(_) => {
// These are fine since we want someone to be able to map a variable to a tag declarator.
}
Expr::TagDeclarator(tag) => {
// TODO: Remove this check. It should be redundant.
tag.clone()
.into_valid_binding_name()
.map_err(|e| ErrMode::Cut(ContextError::from(e)))?;
}
e => {
return Err(ErrMode::Cut(
KclError::Syntax(KclErrorDetails {
source_ranges: vec![SourceRange::from(*arg)],
message: format!("Expected a tag declarator like `$name`, found {:?}", e),
})
.into(),
));
}
},
"TagIdentifier" => match &arg {
Expr::Identifier(_) => {}
Expr::MemberExpression(_) => {}
e => {
return Err(ErrMode::Cut(
KclError::Syntax(KclErrorDetails {
source_ranges: vec![SourceRange::from(*arg)],
message: format!("Expected a tag identifier like `tagName`, found {:?}", e),
})
.into(),
));
}
},
_ => {}
}
}
typecheck_all(std_fn, &args)?;
}
let end = preceded(opt(whitespace), close_paren).parse_next(i)?.end;

View File

@ -67,6 +67,8 @@ pub enum TokenType {
Unknown,
/// The ? symbol, used for optional values.
QuestionMark,
/// The @ symbol.
At,
}
/// Most KCL tokens correspond to LSP semantic tokens (but not all).
@ -93,6 +95,7 @@ impl TryFrom<TokenType> for SemanticTokenType {
| TokenType::DoublePeriod
| TokenType::Hash
| TokenType::Dollar
| TokenType::At
| TokenType::Unknown => {
anyhow::bail!("unsupported token type: {:?}", token_type)
}

View File

@ -92,6 +92,7 @@ pub fn token(i: &mut Input<'_>) -> PResult<Token> {
'}' | ')' | ']' => brace_end,
',' => comma,
'?' => question_mark,
'@' => at,
'0'..='9' => number,
':' => colon,
'.' => alt((number, double_period, period)),
@ -268,6 +269,16 @@ fn question_mark(i: &mut Input<'_>) -> PResult<Token> {
))
}
fn at(i: &mut Input<'_>) -> PResult<Token> {
let (value, range) = '@'.with_span().parse_next(i)?;
Ok(Token::from_range(
range,
i.state.module_id,
TokenType::At,
value.to_string(),
))
}
fn colon(i: &mut Input<'_>) -> PResult<Token> {
let (value, range) = ':'.with_span().parse_next(i)?;
Ok(Token::from_range(