diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 497c5ac9..dc945bed 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 with: - toolchain: "1.72.1" + toolchain: "1.80.1" - run: cargo test # # Check formatting with rustfmt diff --git a/.vscode/settings.json b/.vscode/settings.json index 46f8eff4..c33b5dba 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -153,6 +153,8 @@ "clippy::verbose_file_reads", ], "cSpell.words": [ - "kore" + "kore", + "renderable", + "stdext" ], } \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 0ff3a4d7..d38cf387 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -410,6 +410,7 @@ dependencies = [ "knot_command", "knot_core", "knot_engine", + "knot_language", "knot_plugin_javascript", "predicates", ] diff --git a/analyze/docs/architecture.d2 b/analyze/docs/architecture.d2 new file mode 100644 index 00000000..9a5f7a67 --- /dev/null +++ b/analyze/docs/architecture.d2 @@ -0,0 +1,42 @@ +semantic_analysis: Semantic Analysis +semantic_analysis.shape: oval + +input: Input { + raw: Raw AST + context: Context +} + +output: Output { + typed: Typed AST + types: Types +} + +infer: Type Inference { + infer_weak: Weak Inference + infer_weak.shape: oval + infer_strong: Strong Inference + infer_strong.shape: oval + weak_output: Weak Output + strong_output: Strong Output + + into_typed: Resolve Types + into_typed.shape: oval + canonicalize: Canonicalize + canonicalize.shape: oval + + infer_weak -> weak_output + weak_output -> infer_strong + infer_strong -> infer_strong: incremental + infer_strong -> strong_output + strong_output -> into_typed + strong_output -> canonicalize +} + +input.raw -> infer.infer_weak +input.raw -> infer.into_typed +input.context -> infer.infer_strong +input.context -> infer.into_typed +input.context -> infer.canonicalize +infer.into_typed -> output.typed +infer.canonicalize -> output.types +output.typed -> semantic_analysis diff --git a/analyze/src/error.rs b/analyze/src/error.rs index 7e8fc8bf..f3b0824e 100644 --- a/analyze/src/error.rs +++ b/analyze/src/error.rs @@ -1,6 +1,10 @@ -use lang::{ast, types::Kind, CanonicalId}; +use lang::{ + ast, + types::{self, Kind}, + CanonicalId, +}; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Error { /* inference */ NotInferrable( @@ -17,6 +21,8 @@ pub enum Error { VariantNotFound( // id of the enum declaration CanonicalId, + // names of the valid variants + Vec, // name of the expected variant String, ), @@ -25,6 +31,8 @@ pub enum Error { DeclarationNotFound( // id of the enum declaration CanonicalId, + // names of the valid declarations + Vec, // name of the expected declaration String, ), @@ -40,6 +48,8 @@ pub enum Error { PropertyNotFound( // id of the object CanonicalId, + // names of the valid properties + Vec, // name of the expected property String, ), @@ -56,11 +66,21 @@ pub enum Error { /* callable-related */ /// temporary solution until deeper type inference is implemented - UntypedParameter, + UntypedParameter(String), DefaultValueRejected( - // id of the default value - CanonicalId, + ( + // id of the type definition + CanonicalId, + // the type of the parameter + types::Shape, + ), + ( + // id of the default value + CanonicalId, + // the type of the default value + types::Shape, + ), ), /* function-related */ @@ -72,18 +92,30 @@ pub enum Error { UnexpectedArgument( // id of the argument CanonicalId, + // number of arguments expected + usize, ), MissingArgument( // id of the unfulfilled parameter CanonicalId, + // the type of the parameter + types::Shape, ), ArgumentRejected( - // id of the parameter - CanonicalId, - // id of the argument - CanonicalId, + ( + // id of the parameter + CanonicalId, + // the type of the parameter + types::Shape, + ), + ( + // id of the argument + CanonicalId, + // the type of the argument + types::Shape, + ), ), /* component-related */ @@ -95,6 +127,8 @@ pub enum Error { InvalidComponent( // tag name String, + // the type of the value + types::Shape, ), ComponentTypo( @@ -110,37 +144,65 @@ pub enum Error { ), UnexpectedAttribute( + // id of the unexpected attribute + CanonicalId, // name of the attribute String, ), MissingAttribute( - // id of the unfulfilled parameter + // id of the unfulfilled attribute CanonicalId, + // name of the attribute + String, + // the type of the attribute + types::Shape, ), AttributeRejected( - // id of the parameter - CanonicalId, - // id of the argument - CanonicalId, + ( + // id of the parameter + CanonicalId, + // name of the attribute + String, + // the type of the parameter + types::Shape, + ), + ( + // id of the argument + CanonicalId, + // the type of the argument + types::Shape, + ), ), /* mismatch */ BinaryOperationNotSupported( // operation being performed ast::BinaryOperator, - // id of the left-hand side - CanonicalId, - // id of the right-hand side - CanonicalId, + ( + // id of the left-hand side + CanonicalId, + // the type of the left-hand side + Option, + ), + ( + // id of the right-hand side + CanonicalId, + // the type of the right-hand side + Option, + ), ), UnaryOperationNotSupported( // operation being performed ast::UnaryOperator, - // id of the right-hand side - CanonicalId, + ( + // id of the right-hand side + CanonicalId, + // the type of the right-hand side + Option, + ), ), UnexpectedKind( diff --git a/analyze/src/fixture.rs b/analyze/src/fixture.rs index 3c2fb304..a44f20d7 100644 --- a/analyze/src/fixture.rs +++ b/analyze/src/fixture.rs @@ -3,7 +3,6 @@ use crate::{ AmbientScope, TypeMap as StrongTypes, }; use kore::str; -pub use lang::test::fixture::*; use lang::{ ast, types::{Enumerated, Kind, Type}, @@ -170,7 +169,10 @@ pub mod function { HashMap::from_iter(vec![ ( NodeId(0), - (Kind::Value, weak::Type::Infer(Inference::Parameter)), + ( + Kind::Value, + weak::Type::Infer(Inference::Parameter(str!("first"))), + ), ), (NodeId(1), (Kind::Type, weak::Type::Value(Type::Integer))), ( diff --git a/analyze/src/infer/strong/arithmetic.rs b/analyze/src/infer/strong/arithmetic.rs index 89cd29d3..6c6d7c5e 100644 --- a/analyze/src/infer/strong/arithmetic.rs +++ b/analyze/src/infer/strong/arithmetic.rs @@ -22,7 +22,11 @@ pub fn infer(state: &State, op: ast::BinaryOperator, lhs: CanonicalId, rhs: Cano (Some(Err(_)), _) => Action::Raise(Error::NotInferrable(vec![lhs])), (_, Some(Err(_))) => Action::Raise(Error::NotInferrable(vec![rhs])), - (Some(_), Some(_)) => Action::Raise(Error::BinaryOperationNotSupported(op, lhs, rhs)), + (Some(_), Some(_)) => Action::Raise(Error::BinaryOperationNotSupported( + op, + (lhs, None), + (rhs, None), + )), } } @@ -133,8 +137,8 @@ mod tests { super::infer(&state, OP, CanonicalId::mock(1), CanonicalId::mock(1)), Action::Raise(Error::BinaryOperationNotSupported( OP, - CanonicalId::mock(1), - CanonicalId::mock(1) + (CanonicalId::mock(1), None), + (CanonicalId::mock(1), None) )) ); } diff --git a/analyze/src/infer/strong/mock.rs b/analyze/src/infer/strong/mock.rs index ceb7bf1b..f722aaff 100644 --- a/analyze/src/infer/strong/mock.rs +++ b/analyze/src/infer/strong/mock.rs @@ -7,7 +7,7 @@ pub const FRAGMENTS: &BTreeMap = &BTreeMap::new(); impl<'a> State<'a> { #[allow(clippy::type_complexity)] - pub fn mock(context: &'a Context) -> State<'a> { + pub fn mock(context: &'a Context) -> Self { State { context, fragments: FRAGMENTS, @@ -22,7 +22,7 @@ impl<'a> State<'a> { pub fn from_types( context: &'a Context, types: Vec<(NodeId, (Kind, Result))>, - ) -> State<'a> { + ) -> Self { State { types: BTreeMap::from_iter(types), ..Self::mock(context) diff --git a/analyze/src/infer/strong/partial.rs b/analyze/src/infer/strong/partial.rs index 3599a421..84c47ea4 100644 --- a/analyze/src/infer/strong/partial.rs +++ b/analyze/src/infer/strong/partial.rs @@ -93,11 +93,11 @@ pub fn infer_types<'a>(ctx: &Context, prev: State<'a>) -> State<'a> { } => import::infer(ctx, source, path), NodeDescriptor { - weak: weak::Type::Infer(Inference::Parameter), + weak: weak::Type::Infer(Inference::Parameter(name)), .. } => { // TODO: replace this with actual type inference - Action::Raise(Error::UntypedParameter) + Action::Raise(Error::UntypedParameter(name.to_owned())) } }; diff --git a/analyze/src/infer/strong/property.rs b/analyze/src/infer/strong/property.rs index 97f27191..05229db3 100644 --- a/analyze/src/infer/strong/property.rs +++ b/analyze/src/infer/strong/property.rs @@ -21,7 +21,11 @@ fn infer_module( Some((.., id)) => Action::Raise(Error::UnexpectedKind(*id, *allowed_kind)), - None => Action::Raise(Error::DeclarationNotFound(*module, property.to_owned())), + None => Action::Raise(Error::DeclarationNotFound( + *module, + declarations.iter().map(|x| x.0.clone()).collect(), + property.to_owned(), + )), } } @@ -38,7 +42,18 @@ fn infer_object( ObjectTypeEntry::Optional(..) => unimplemented!("need to wrap this in an option"), }, - None => Action::Raise(Error::PropertyNotFound(*object, property.to_owned())), + None => Action::Raise(Error::PropertyNotFound( + *object, + entries + .iter() + .map(|x| match x { + ObjectTypeEntry::Required(name, ..) | ObjectTypeEntry::Optional(name, ..) => { + name.clone() + } + }) + .collect(), + property.to_owned(), + )), } } @@ -52,7 +67,11 @@ fn infer_enumerated( Enumerated::Variant(parameters.clone(), *enumerated), ))), - None => Action::Raise(Error::VariantNotFound(*enumerated, property.to_owned())), + None => Action::Raise(Error::VariantNotFound( + *enumerated, + variants.iter().map(|x| x.0.clone()).collect(), + property.to_owned(), + )), } } @@ -140,7 +159,7 @@ mod tests { ( Kind::Value, Ok(Type::Value(types::Type::Enumerated( - Enumerated::Declaration(vec![]), + Enumerated::Declaration(vec![(str!("bar"), vec![CanonicalId::mock(2)])]), ))), ), )], @@ -148,7 +167,11 @@ mod tests { assert_eq!( super::infer(&state, CanonicalId::mock(1), "foo", &Kind::Value), - Action::Raise(Error::VariantNotFound(CanonicalId::mock(1), str!("foo"))) + Action::Raise(Error::VariantNotFound( + CanonicalId::mock(1), + vec![str!("bar")], + str!("foo") + )) ); } @@ -189,13 +212,22 @@ mod tests { &ctx, vec![( NodeId(1), - (Kind::Value, Ok(Type::Value(types::Type::Object(vec![])))), + ( + Kind::Value, + Ok(Type::Value(types::Type::Object(vec![ + ObjectTypeEntry::Required(str!("bar"), CanonicalId::mock(2)), + ]))), + ), )], ); assert_eq!( super::infer(&state, CanonicalId::mock(1), "foo", &Kind::Value), - Action::Raise(Error::PropertyNotFound(CanonicalId::mock(1), str!("foo"))) + Action::Raise(Error::PropertyNotFound( + CanonicalId::mock(1), + vec![str!("bar")], + str!("foo") + )) ); } @@ -256,7 +288,14 @@ mod tests { &ctx, vec![( NodeId(1), - (Kind::Value, Ok(Type::Value(types::Type::Module(vec![])))), + ( + Kind::Value, + Ok(Type::Value(types::Type::Module(vec![( + str!("bar"), + Kind::Value, + CanonicalId::mock(2), + )]))), + ), )], ); @@ -264,6 +303,7 @@ mod tests { super::infer(&state, CanonicalId::mock(1), "foo", &Kind::Value), Action::Raise(Error::DeclarationNotFound( CanonicalId::mock(1), + vec![str!("bar")], str!("foo") )) ); diff --git a/analyze/src/infer/weak/data.rs b/analyze/src/infer/weak/data.rs index 9aa19757..712cbb1d 100644 --- a/analyze/src/infer/weak/data.rs +++ b/analyze/src/infer/weak/data.rs @@ -17,7 +17,7 @@ pub enum Inference { Module(Vec), View(Vec), ViewType(NodeId), - Parameter, + Parameter(String), } /// the inferred type for nodes in a weakly typed AST diff --git a/analyze/src/infer/weak/declaration.rs b/analyze/src/infer/weak/declaration.rs index 21d8fa3d..60760fc9 100644 --- a/analyze/src/infer/weak/declaration.rs +++ b/analyze/src/infer/weak/declaration.rs @@ -23,7 +23,7 @@ impl ToWeak for ast::Parameter { .. } => Type::Inherit(*x), - Self { .. } => Type::Infer(Inference::Parameter), + Self { binding, .. } => Type::Infer(Inference::Parameter(binding.to_owned())), }, ) } @@ -103,7 +103,7 @@ mod tests { fn parameter_inference() { assert_eq!( ast::Parameter::new(str!("foo"), None, None).to_weak(), - (Kind::Value, Type::Infer(Inference::Parameter)) + (Kind::Value, Type::Infer(Inference::Parameter(str!("foo")))) ); } diff --git a/analyze/src/infer/weak/mod.rs b/analyze/src/infer/weak/mod.rs index 0f497fdf..baee9e96 100644 --- a/analyze/src/infer/weak/mod.rs +++ b/analyze/src/infer/weak/mod.rs @@ -4,7 +4,7 @@ mod expression; mod to_weak; mod types; -pub use data::{Inference, Output, Type, Weak}; +pub use data::{Inference, Output, Type}; use lang::{FragmentMap, NodeId}; use to_weak::ToWeak; diff --git a/analyze/src/infer/weak/types.rs b/analyze/src/infer/weak/types.rs index b211588d..1ec242d2 100644 --- a/analyze/src/infer/weak/types.rs +++ b/analyze/src/infer/weak/types.rs @@ -106,7 +106,7 @@ mod tests { fn parameter_inference() { assert_eq!( ast::Parameter::new(str!("foo"), None, None).to_weak(), - (Kind::Value, Type::Infer(Inference::Parameter)) + (Kind::Value, Type::Infer(Inference::Parameter(str!("foo")))) ); } diff --git a/analyze/src/semantic/component.rs b/analyze/src/semantic/component.rs index 34c9577d..47c14b76 100644 --- a/analyze/src/semantic/component.rs +++ b/analyze/src/semantic/component.rs @@ -46,7 +46,8 @@ pub fn analyze( errors.push(Error::ComponentTypo(start_tag.clone(), end_tag.clone())); } - if let Type::View(parameters) = ctx.type_of() { + let type_ = ctx.type_of(); + if let Type::View(parameters) = type_ { let mut unsatisfied_parameters = parameters .iter() .map(|x| (x.name().to_owned(), x)) @@ -62,8 +63,12 @@ pub fn analyze( if parameter_type != argument_type { errors.push(Error::AttributeRejected( - *parameter.value().id(), - *attribute.0.id(), + ( + *parameter.value().id(), + parameter.name().to_owned(), + parameter.value().to_shape(), + ), + (*attribute.0.id(), attribute.0.type_of().to_shape()), )); } } else { @@ -75,17 +80,22 @@ pub fn analyze( sorted_parameters.sort_by(|l, r| l.0.cmp(&r.0)); errors.extend(sorted_parameters.into_iter().filter_map(|(_, x)| { - x.is_required() - .then_some(Error::MissingAttribute(*x.value().id())) + let value = x.value(); + + x.is_required().then_some(Error::MissingAttribute( + *value.id(), + x.name().to_owned(), + value.type_of().to_shape(), + )) })); errors.extend( - unexpected_attributes - .into_iter() - .map(|x| Error::UnexpectedAttribute(x.0.value().name().to_owned())), + unexpected_attributes.into_iter().map(|x| { + Error::UnexpectedAttribute(*x.0.id(), x.0.value().name().to_owned()) + }), ); } else { - errors.push(Error::InvalidComponent(start_tag.clone())); + errors.push(Error::InvalidComponent(start_tag.clone(), type_.to_shape())); } (!errors.is_empty()).then_some(errors) diff --git a/analyze/src/semantic/expression.rs b/analyze/src/semantic/expression.rs index 30c32f8e..742e6d1b 100644 --- a/analyze/src/semantic/expression.rs +++ b/analyze/src/semantic/expression.rs @@ -31,7 +31,10 @@ pub fn analyze( (UnaryOperator::Absolute | UnaryOperator::Negate, Type::Integer | Type::Float) => None, - _ => Some(vec![Error::UnaryOperationNotSupported(*op, *x.id())]), + _ => Some(vec![Error::UnaryOperationNotSupported( + *op, + (*x.id(), Some(x.type_of().to_shape())), + )]), }, Expression::BinaryOperation(op, lhs, rhs) => { @@ -60,10 +63,10 @@ pub fn analyze( None } - _ => Some(vec![Error::BinaryOperationNotSupported( + (_, lhs_type, rhs_type) => Some(vec![Error::BinaryOperationNotSupported( *op, - *lhs.id(), - *rhs.id(), + (*lhs.id(), Some(lhs_type.to_shape())), + (*rhs.id(), Some(rhs_type.to_shape())), )]), } } @@ -82,21 +85,30 @@ pub fn analyze( match pair { // TODO: should this use a more nuanced approach for comparing types? // how will this handle enumerators for example? - (Some(parameter), Some(argument)) - if parameter.to_shape() == argument.type_of().to_shape() => {} - (Some(parameter), Some(argument)) => { - errors.push(Error::ArgumentRejected(*parameter.id(), *argument.id())); + let parameter_shape = parameter.to_shape(); + let argument_shape = argument.type_of().to_shape(); + + if parameter_shape != argument_shape { + errors.push(Error::ArgumentRejected( + (*parameter.id(), parameter_shape), + (*argument.id(), argument_shape), + )); + } } (None, Some(argument)) => { - errors.push(Error::UnexpectedArgument(*argument.id())); + errors + .push(Error::UnexpectedArgument(*argument.id(), parameters.len())); } // TODO: this doesn't take into account default values // need to bake it into the type definition (Some(parameter), None) => { - errors.push(Error::MissingArgument(*parameter.id())); + errors.push(Error::MissingArgument( + *parameter.id(), + parameter.to_shape(), + )); } (None, None) => break, diff --git a/analyze/src/semantic/parameter.rs b/analyze/src/semantic/parameter.rs index 2fbb8cb7..68a42a3e 100644 --- a/analyze/src/semantic/parameter.rs +++ b/analyze/src/semantic/parameter.rs @@ -21,9 +21,15 @@ pub fn analyze( _: &Visitor, ) -> Option> { match (value_type.as_ref(), default_value.as_ref()) { - (Some(typdef), Some(default)) => (typdef.type_of().to_shape() - != default.type_of().to_shape()) - .then_some(vec![Error::DefaultValueRejected(*default.id())]), + (Some(typedef), Some(default)) => { + let typedef_shape = typedef.type_of().to_shape(); + let default_shape = default.type_of().to_shape(); + + (typedef_shape != default_shape).then_some(vec![Error::DefaultValueRejected( + (*typedef.id(), typedef_shape), + (*default.id(), default_shape), + )]) + } _ => None, } diff --git a/analyze/tests/semantic_errors_test.rs b/analyze/tests/semantic_errors_test.rs index 3b7969ca..337c58cd 100644 --- a/analyze/tests/semantic_errors_test.rs +++ b/analyze/tests/semantic_errors_test.rs @@ -1,6 +1,6 @@ use knot_analyze::{AmbientMap, Context, Error, ModuleMap, Result, TypeMap}; use kore::{assert_eq, str}; -use lang::{ast, CanonicalId, NodeId}; +use lang::{ast, types, CanonicalId, NodeId}; #[derive(Default)] struct Mock { @@ -56,7 +56,11 @@ const bar = foo.Other;"; Err(vec![ ( NodeId(3), - Error::VariantNotFound(CanonicalId::mock(2), str!("Other")) + Error::VariantNotFound( + CanonicalId::mock(2), + vec![str!("Integer"), str!("Empty")], + str!("Other") + ) ), (NodeId(4), Error::NotInferrable(vec![CanonicalId::mock(3)])) ]) @@ -66,7 +70,9 @@ const bar = foo.Other;"; #[test] fn declaration_not_found() { let source = " -module foo {} +module foo { + const buzz = 123; +} const bar = foo.fizz;"; @@ -74,10 +80,10 @@ const bar = foo.fizz;"; Mock::default().parse_and_analyze(source), Err(vec![ ( - NodeId(3), - Error::DeclarationNotFound(CanonicalId::mock(2), str!("fizz")) + NodeId(5), + Error::DeclarationNotFound(CanonicalId::mock(4), vec![str!("buzz")], str!("fizz")) ), - (NodeId(4), Error::NotInferrable(vec![CanonicalId::mock(3)])) + (NodeId(6), Error::NotInferrable(vec![CanonicalId::mock(5)])) ]) ); } @@ -106,7 +112,7 @@ fn property_not_found() { Mock::default().parse_and_analyze(source), Err(vec![( NodeId(4), - Error::PropertyNotFound(CanonicalId::mock(3), str!("fizz")) + Error::PropertyNotFound(CanonicalId::mock(3), vec![str!("bar")], str!("fizz")) ),]) ); } @@ -157,7 +163,7 @@ fn untyped_parameter() { assert_eq!( Mock::default().parse_and_analyze(source), - Err(vec![(NodeId(0), Error::UntypedParameter)]) + Err(vec![(NodeId(0), Error::UntypedParameter(str!("bar")))]) ); } @@ -169,7 +175,10 @@ fn default_value_rejected() { Mock::default().parse_and_analyze(source), Err(vec![( NodeId(2), - Error::DefaultValueRejected(CanonicalId::mock(1)) + Error::DefaultValueRejected( + (CanonicalId::mock(0), types::Shape(types::Type::Integer)), + (CanonicalId::mock(1), types::Shape(types::Type::Boolean)) + ) )]) ); } @@ -185,7 +194,7 @@ const bar = foo(123);"; Mock::default().parse_and_analyze(source), Err(vec![( NodeId(4), - Error::UnexpectedArgument(CanonicalId::mock(3)) + Error::UnexpectedArgument(CanonicalId::mock(3), 0) )]) ); } @@ -200,8 +209,14 @@ const bar = foo(123, true);"; assert_eq!( Mock::default().parse_and_analyze(source), Err(vec![ - (NodeId(5), Error::UnexpectedArgument(CanonicalId::mock(3))), - (NodeId(5), Error::UnexpectedArgument(CanonicalId::mock(4))) + ( + NodeId(5), + Error::UnexpectedArgument(CanonicalId::mock(3), 0) + ), + ( + NodeId(5), + Error::UnexpectedArgument(CanonicalId::mock(4), 0) + ) ]) ); } @@ -217,7 +232,7 @@ const bar = foo();"; Mock::default().parse_and_analyze(source), Err(vec![( NodeId(5), - Error::MissingArgument(CanonicalId::mock(0)) + Error::MissingArgument(CanonicalId::mock(0), types::Shape(types::Type::Integer)) )]) ); } @@ -232,8 +247,14 @@ const bar = foo();"; assert_eq!( Mock::default().parse_and_analyze(source), Err(vec![ - (NodeId(7), Error::MissingArgument(CanonicalId::mock(0))), - (NodeId(7), Error::MissingArgument(CanonicalId::mock(2))) + ( + NodeId(7), + Error::MissingArgument(CanonicalId::mock(0), types::Shape(types::Type::Integer)) + ), + ( + NodeId(7), + Error::MissingArgument(CanonicalId::mock(2), types::Shape(types::Type::Float)) + ) ]) ); } @@ -249,7 +270,10 @@ const bar = foo(true);"; Mock::default().parse_and_analyze(source), Err(vec![( NodeId(6), - Error::ArgumentRejected(CanonicalId::mock(0), CanonicalId::mock(5)) + Error::ArgumentRejected( + (CanonicalId::mock(0), types::Shape(types::Type::Integer)), + (CanonicalId::mock(5), types::Shape(types::Type::Boolean)) + ) )]) ); } @@ -266,11 +290,17 @@ const bar = foo(true, nil);"; Err(vec![ ( NodeId(9), - Error::ArgumentRejected(CanonicalId::mock(0), CanonicalId::mock(7)) + Error::ArgumentRejected( + (CanonicalId::mock(0), types::Shape(types::Type::Integer)), + (CanonicalId::mock(7), types::Shape(types::Type::Boolean)) + ) ), ( NodeId(9), - Error::ArgumentRejected(CanonicalId::mock(2), CanonicalId::mock(8)) + Error::ArgumentRejected( + (CanonicalId::mock(2), types::Shape(types::Type::Float)), + (CanonicalId::mock(8), types::Shape(types::Type::Nil)) + ) ) ]) ); @@ -285,7 +315,10 @@ const bar = ;"; assert_eq!( Mock::default().parse_and_analyze(source), - Err(vec![(NodeId(4), Error::UnexpectedAttribute(str!("a")))]) + Err(vec![( + NodeId(4), + Error::UnexpectedAttribute(CanonicalId::mock(3), str!("a")) + )]) ); } @@ -299,8 +332,14 @@ const bar = ;"; assert_eq!( Mock::default().parse_and_analyze(source), Err(vec![ - (NodeId(6), Error::UnexpectedAttribute(str!("a"))), - (NodeId(6), Error::UnexpectedAttribute(str!("b"))) + ( + NodeId(6), + Error::UnexpectedAttribute(CanonicalId::mock(3), str!("a")) + ), + ( + NodeId(6), + Error::UnexpectedAttribute(CanonicalId::mock(5), str!("b")) + ) ]) ); } @@ -316,7 +355,11 @@ const bar = ;"; Mock::default().parse_and_analyze(source), Err(vec![( NodeId(4), - Error::MissingAttribute(CanonicalId::mock(0)) + Error::MissingAttribute( + CanonicalId::mock(0), + str!("a"), + types::Shape(types::Type::Integer) + ) )]) ); } @@ -331,8 +374,22 @@ const bar = ;"; assert_eq!( Mock::default().parse_and_analyze(source), Err(vec![ - (NodeId(6), Error::MissingAttribute(CanonicalId::mock(0))), - (NodeId(6), Error::MissingAttribute(CanonicalId::mock(2))) + ( + NodeId(6), + Error::MissingAttribute( + CanonicalId::mock(0), + str!("a"), + types::Shape(types::Type::Integer) + ) + ), + ( + NodeId(6), + Error::MissingAttribute( + CanonicalId::mock(2), + str!("b"), + types::Shape(types::Type::Float) + ) + ) ]) ); } @@ -348,7 +405,14 @@ const bar = ;"; Mock::default().parse_and_analyze(source), Err(vec![( NodeId(6), - Error::AttributeRejected(CanonicalId::mock(0), CanonicalId::mock(5)) + Error::AttributeRejected( + ( + CanonicalId::mock(0), + str!("a"), + types::Shape(types::Type::Integer) + ), + (CanonicalId::mock(5), types::Shape(types::Type::Boolean)) + ) )]) ); } @@ -365,11 +429,25 @@ const bar = ;"; Err(vec![ ( NodeId(10), - Error::AttributeRejected(CanonicalId::mock(0), CanonicalId::mock(7)) + Error::AttributeRejected( + ( + CanonicalId::mock(0), + str!("a"), + types::Shape(types::Type::Integer) + ), + (CanonicalId::mock(7), types::Shape(types::Type::Boolean)) + ) ), ( NodeId(10), - Error::AttributeRejected(CanonicalId::mock(2), CanonicalId::mock(9)) + Error::AttributeRejected( + ( + CanonicalId::mock(2), + str!("b"), + types::Shape(types::Type::Float) + ), + (CanonicalId::mock(9), types::Shape(types::Type::Nil)) + ) ) ]) ); @@ -400,7 +478,10 @@ const bar = ;"; assert_eq!( Mock::default().parse_and_analyze(source), - Err(vec![(NodeId(2), Error::InvalidComponent(str!("foo")))]) + Err(vec![( + NodeId(2), + Error::InvalidComponent(str!("foo"), types::Shape(types::Type::Integer)) + )]) ); } @@ -431,8 +512,8 @@ fn binary_operation_not_supported() { NodeId(2), Error::BinaryOperationNotSupported( ast::BinaryOperator::Add, - CanonicalId::mock(0), - CanonicalId::mock(1) + (CanonicalId::mock(0), None), + (CanonicalId::mock(1), None) ) ), (NodeId(3), Error::NotInferrable(vec![CanonicalId::mock(2)])) @@ -448,7 +529,13 @@ fn unary_operation_not_supported() { Mock::default().parse_and_analyze(source), Err(vec![( NodeId(1), - Error::UnaryOperationNotSupported(ast::UnaryOperator::Not, CanonicalId::mock(0),) + Error::UnaryOperationNotSupported( + ast::UnaryOperator::Not, + ( + CanonicalId::mock(0), + Some(types::Shape(types::Type::Integer)) + ) + ) ),]) ); } diff --git a/bin/Cargo.toml b/bin/Cargo.toml index 9f79d938..2ecb51f8 100644 --- a/bin/Cargo.toml +++ b/bin/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" clap = { version = "4.4.18", features = ["derive"] } exitcode = "1.1.2" kore = { package = "knot_core", path = "../kore" } +lang = { package = "knot_language", path = "../language" } engine = { package = "knot_engine", path = "../engine" } command = { package = "knot_command", path = "../command" } js = { package = "knot_plugin_javascript", path = "../plugin_javascript" } diff --git a/bin/src/build.rs b/bin/src/build.rs index ffa44726..218ed228 100644 --- a/bin/src/build.rs +++ b/bin/src/build.rs @@ -1,11 +1,12 @@ use crate::{ args::Target, config::Config, + log, path::{get_out_dir, get_root_dir, get_source_dir, validate_entrypoint}, - reporter::{eprint_configuration, Phase}, }; -use command::{ast, build}; +use command::{build, Phase}; use kore::Generator; +use lang::ast; use std::path::Path; pub struct Args<'a> { @@ -26,7 +27,7 @@ impl<'a> Args<'a> { target, } = self; - eprint_configuration(vec![ + log::configuration(vec![ ("root_dir", Config::Path(root_dir)), ( "source_dir", diff --git a/bin/src/check.rs b/bin/src/check.rs index c24df7ff..9b514d97 100644 --- a/bin/src/check.rs +++ b/bin/src/check.rs @@ -1,9 +1,9 @@ use crate::{ config::Config, + log, path::{get_root_dir, get_source_dir, validate_entrypoint}, - reporter::{eprint_configuration, Phase}, }; -use command::check; +use command::{check, Phase}; use std::path::Path; pub struct Args<'a> { @@ -20,7 +20,7 @@ impl<'a> Args<'a> { entry, } = self; - eprint_configuration(vec![ + log::configuration(vec![ ("root_dir", Config::Path(root_dir)), ( "source_dir", diff --git a/bin/src/format.rs b/bin/src/format.rs index c6fd395b..1fc136cc 100644 --- a/bin/src/format.rs +++ b/bin/src/format.rs @@ -1,9 +1,5 @@ -use crate::{ - config::Config, - path::get_root_dir, - reporter::{eprint_configuration, Phase}, -}; -use command::format; +use crate::{config::Config, log, path::get_root_dir}; +use command::{format, Phase}; use std::path::Path; pub struct Args<'a> { @@ -15,7 +11,7 @@ impl<'a> Args<'a> { fn report(&self) { let Self { root_dir, glob } = self; - eprint_configuration(vec![ + log::configuration(vec![ ("root_dir", Config::Path(root_dir)), ("glob", Config::String(glob)), ]); diff --git a/bin/src/log.rs b/bin/src/log.rs new file mode 100644 index 00000000..48277e17 --- /dev/null +++ b/bin/src/log.rs @@ -0,0 +1,6 @@ +use crate::config::{Config, ConfigList}; +use command::Phase; + +pub fn configuration(configs: Vec<(&'static str, Config)>) { + eprintln!("{}{}", Phase::Configuration, ConfigList::new(configs)); +} diff --git a/bin/src/main.rs b/bin/src/main.rs index e6b2a4e3..438efcd9 100644 --- a/bin/src/main.rs +++ b/bin/src/main.rs @@ -3,15 +3,14 @@ mod build; mod check; mod config; mod format; +mod log; mod path; -mod reporter; use args::{Args, Command}; use clap::Parser; +use command::Phase; use kore::color::Highlight; -use crate::reporter::Phase; - fn main() { let args = Args::parse(); let command = format!("knot:{}", args.command); @@ -53,8 +52,8 @@ fn main() { Ok(()) => { eprintln!("{}{} - passed \u{2705}", Phase::Result, command.focus()); } - Err(errs) => { - reporter::eprint_report(&errs); + Err(report) => { + eprint!("{}{}", Phase::Report, report); eprintln!("{}{} - failed \u{274c}", Phase::Result, command.focus()); diff --git a/bin/src/path.rs b/bin/src/path.rs index a1c16fb7..4a6b4a28 100644 --- a/bin/src/path.rs +++ b/bin/src/path.rs @@ -1,27 +1,30 @@ -use engine::{Error, Result}; -use std::{ - fs, - path::{Path, PathBuf}, -}; +use command::AssertExists; +use engine::{ConfigurationError, Report, Result}; +use std::path::{Path, PathBuf}; pub fn get_root_dir(path: &Path) -> Result { if path.is_absolute() { path.to_path_buf() } else { - path.canonicalize() - .map_err(|_| vec![Error::RootDirectoryNotFound(path.to_path_buf())])? + path.canonicalize().map_err(|_| { + Report::Configuration(ConfigurationError::RootDirectoryNotFound( + path.to_path_buf(), + )) + })? } - .assert_dir_exists(Error::RootDirectoryNotFound) + .assert_dir_exists(ConfigurationError::RootDirectoryNotFound) } pub fn get_source_dir(root_dir: &Path, path: &Path) -> Result { if path.is_absolute() { - return Err(vec![Error::SourceDirectoryNotRelative(path.to_path_buf())]); + return Err(Box::new(Report::Configuration( + ConfigurationError::SourceDirectoryNotRelative(path.to_path_buf()), + ))); } root_dir .join(path) - .assert_dir_exists(Error::SourceDirectoryNotFound) + .assert_dir_exists(ConfigurationError::SourceDirectoryNotFound) } pub fn get_out_dir(root_dir: &Path, path: &Path) -> PathBuf { @@ -34,47 +37,14 @@ pub fn get_out_dir(root_dir: &Path, path: &Path) -> PathBuf { pub fn validate_entrypoint(source_dir: &Path, path: &Path) -> Result<()> { if path.is_absolute() { - return Err(vec![Error::EntrypointNotRelative(path.to_path_buf())]); + return Err(Box::new(Report::Configuration( + ConfigurationError::EntrypointNotRelative(path.to_path_buf()), + ))); } source_dir .join(path) - .assert_file_exists(Error::EntrypointNotFound)?; + .assert_file_exists(ConfigurationError::EntrypointNotFound)?; Ok(()) } - -pub trait AssertExists: Sized { - fn assert_dir_exists(self, factory: F) -> Result - where - F: Fn(Self) -> Error; - - fn assert_file_exists(self, factory: F) -> Result - where - F: Fn(Self) -> Error; -} - -impl AssertExists for T -where - T: AsRef, -{ - fn assert_dir_exists(self, factory: F) -> Result - where - F: Fn(Self) -> Error, - { - match fs::metadata(&self) { - Ok(meta) if meta.is_dir() => Ok(self), - _ => Err(vec![factory(self)]), - } - } - - fn assert_file_exists(self, factory: F) -> Result - where - F: Fn(Self) -> Error, - { - match fs::metadata(&self) { - Ok(meta) if meta.is_file() => Ok(self), - _ => Err(vec![factory(self)]), - } - } -} diff --git a/bin/src/reporter.rs b/bin/src/reporter.rs deleted file mode 100644 index 9f8cbded..00000000 --- a/bin/src/reporter.rs +++ /dev/null @@ -1,60 +0,0 @@ -use crate::config::{Config, ConfigList}; -use command::Error; -use kore::color::{Colorize, Highlight}; -use std::fmt::Display; - -// const ERROR_HEADER: &str = "\u{2554}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2557} -// \u{2551} FAILED \u{2551} -// \u{255a}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{2550}\u{255d}"; - -pub enum Phase { - Configuration, - Execution, - Report, - Result, -} - -impl Display for Phase { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - writeln!( - f, - "{}\n", - format!( - "[ {} ]", - match self { - Self::Configuration => "configuration", - Self::Execution => "execution", - Self::Report => "report", - Self::Result => "result", - } - ) - .subtle() - ) - } -} - -pub fn eprint_report(errors: &[Error]) { - let error_count = errors.len(); - let error_count_bumper = - format!("finished with {} error(s)", error_count.to_string().bold()).error(); - - eprintln!( - "{}{}\n", - Phase::Report, - // ERROR_HEADER.error(), - error_count_bumper - ); - - for (index, error) in errors.iter().enumerate() { - eprintln!( - "{index} {error}\n", - index = format!("{})", index + 1).error() - ); - } - - eprintln!("{}\n", error_count_bumper); -} - -pub fn eprint_configuration(configs: Vec<(&'static str, Config)>) { - eprintln!("{}{}", Phase::Configuration, ConfigList::new(configs)); -} diff --git a/bin/tests/analysis_errors_test.rs b/bin/tests/analysis_errors_test.rs new file mode 100644 index 00000000..dca22293 --- /dev/null +++ b/bin/tests/analysis_errors_test.rs @@ -0,0 +1,257 @@ +mod common; + +use assert_cmd::Command; +use assert_fs::{ + fixture::{PathChild, TempDir}, + prelude::FileWriteFile, +}; +use std::path::{Path, PathBuf}; + +fn main_file(folder: &str) -> PathBuf { + Path::new("../examples/invalid") + .join(folder) + .join("src/main.kn") +} + +#[test] +fn not_found() -> Result<(), Box> { + let root_dir = TempDir::new()?; + root_dir + .child("src/main.kn") + .write_file(&main_file("401_not_found"))?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("check"); + + cmd.assert().failure().stderr(predicates::str::contains( + "Not Found (E#401) + + This expression references a variable named BAR which does not exist.", + )); + + root_dir.close()?; + + Ok(()) +} + +#[test] +fn variant_not_found() -> Result<(), Box> { + let root_dir = TempDir::new()?; + root_dir + .child("src/main.kn") + .write_file(&main_file("402_variant_not_found"))?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("check"); + + cmd.assert().failure().stderr(predicates::str::contains( + "Variant Not Found (E#402) + + There is no variant named Bar declared on this enumerated type.", + )); + + root_dir.close()?; + + Ok(()) +} + +#[test] +fn declaration_not_found() -> Result<(), Box> { + let root_dir = TempDir::new()?; + root_dir + .child("src/main.kn") + .write_file(&main_file("403_declaration_not_found"))?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("check"); + + cmd.assert().failure().stderr(predicates::str::contains( + "Declaration Not Found (E#403) + + There are no entities named bar declared in this module.", + )); + + root_dir.close()?; + + Ok(()) +} + +#[test] +fn not_indexable() -> Result<(), Box> { + let root_dir = TempDir::new()?; + root_dir + .child("src/main.kn") + .write_file(&main_file("404_not_indexable"))?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("check"); + + cmd.assert().failure().stderr(predicates::str::contains( + "Not Indexable (E#404) + + The property foo cannot be accessed. This value does not support properties.", + )); + + root_dir.close()?; + + Ok(()) +} + +#[ignore = "cannot trigger this error"] +#[test] +fn property_not_found() -> Result<(), Box> { + let root_dir = TempDir::new()?; + root_dir + .child("src/main.kn") + .write_file(&main_file("405_property_not_found"))?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("check"); + + cmd.assert().failure().stderr(predicates::str::contains( + "Property Not Found (E#405) + + There is no property named fizz on this value.", + )); + + root_dir.close()?; + + Ok(()) +} + +#[test] +fn duplicate_property() -> Result<(), Box> { + let root_dir = TempDir::new()?; + root_dir + .child("src/main.kn") + .write_file(&main_file("406_duplicate_property"))?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("check"); + + cmd.assert().failure().stderr(predicates::str::contains( + "Duplicate Property (E#406) + + This expression includes multiple properties named bar.", + )); + + root_dir.close()?; + + Ok(()) +} + +#[test] +fn not_spreadable() -> Result<(), Box> { + let root_dir = TempDir::new()?; + root_dir + .child("src/main.kn") + .write_file(&main_file("407_not_spreadable"))?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("check"); + + cmd.assert().failure().stderr(predicates::str::contains( + "Not Spreadable (E#407) + + This expression cannot be spread because it is not object-like.", + )); + + root_dir.close()?; + + Ok(()) +} + +#[test] +fn untyped_parameter() -> Result<(), Box> { + let root_dir = TempDir::new()?; + root_dir + .child("src/main.kn") + .write_file(&main_file("408_untyped_parameter"))?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("check"); + + cmd.assert().failure().stderr(predicates::str::contains( + "Untyped Parameter (E#408) + + The parameter bar is missing a type annotation.", + )); + + root_dir.close()?; + + Ok(()) +} + +#[test] +fn default_value_rejected() -> Result<(), Box> { + let root_dir = TempDir::new()?; + root_dir + .child("src/main.kn") + .write_file(&main_file("409_default_value_rejected"))?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("check"); + + cmd.assert().failure().stderr(predicates::str::contains( + "Default Value Rejected (E#409) + + The type of this default value does not match the parameter's type.", + )); + + root_dir.close()?; + + Ok(()) +} + +#[test] +fn not_callable() -> Result<(), Box> { + let root_dir = TempDir::new()?; + root_dir + .child("src/main.kn") + .write_file(&main_file("410_not_callable"))?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("check"); + + cmd.assert().failure().stderr(predicates::str::contains( + "Not Callable (E#410) + + This expression is not a function and cannot be called.", + )); + + root_dir.close()?; + + Ok(()) +} + +#[test] +fn unexpected_argument() -> Result<(), Box> { + let root_dir = TempDir::new()?; + root_dir + .child("src/main.kn") + .write_file(&main_file("411_unexpected_argument"))?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("check"); + + cmd.assert().failure().stderr(predicates::str::contains( + "Unexpected Argument (E#411) + + This argument was not expected by the function call.", + )); + + root_dir.close()?; + + Ok(()) +} diff --git a/bin/tests/build_errors_test.rs b/bin/tests/build_args_errors_test.rs similarity index 85% rename from bin/tests/build_errors_test.rs rename to bin/tests/build_args_errors_test.rs index fcd51f8f..8f00df0f 100644 --- a/bin/tests/build_errors_test.rs +++ b/bin/tests/build_args_errors_test.rs @@ -1,10 +1,17 @@ -use std::path::{Path, PathBuf, StripPrefixError}; +use std::{ + env, + path::{Path, PathBuf, StripPrefixError}, +}; use assert_cmd::Command; use assert_fs::fixture::{FileWriteStr, PathChild, PathCreateDir, TempDir}; fn prefix_private(path: &Path) -> Result { - Ok(Path::new("/private").join(path.strip_prefix("/")?)) + if env::consts::ARCH == "aarch64" { + return Ok(Path::new("/private").join(path.strip_prefix("/")?)); + } + + Ok(path.to_path_buf()) } #[test] @@ -21,7 +28,7 @@ fn root_directory_not_found() -> Result<(), Box> { .stderr(predicates::str::contains(format!( "Root Directory Not Found (E#111) -no folder was found at the path {}", + No folder was found at the path {}.", root_dir.display() ))); @@ -44,7 +51,7 @@ fn source_directory_not_found() -> Result<(), Box> { .stderr(predicates::str::contains(format!( "Source Directory Not Found (E#112) -no folder was found at the path {}", + No folder was found at the path {}.", prefix_private(&source_dir)?.display() ))); @@ -68,7 +75,7 @@ fn source_directory_not_relative() -> Result<(), Box> { .stderr(predicates::str::contains(format!( "Source Directory Not Relative (E#113) -the path to the source directory should be relative to the root_dir but found {}", + The path to the source directory should be relative to the root_dir but found {}.", source_dir.display() ))); @@ -93,7 +100,7 @@ fn entrypoint_not_found() -> Result<(), Box> { .stderr(predicates::str::contains(format!( "Entrypoint Not Found (E#114) -no module was found at the path {}", + No module was found at the path {}.", prefix_private(&entry)?.display() ))); @@ -118,7 +125,7 @@ fn entrypoint_not_relative() -> Result<(), Box> { .stderr(predicates::str::contains(format!( "Entrypoint Not Relative (E#115) -the path to the entrypoint should be relative to the source_dir but found {}", + The path to the entrypoint should be relative to the source_dir but found {}.", entry.display() ))); diff --git a/bin/tests/common/mod.rs b/bin/tests/common/mod.rs index dd026025..90d9435d 100644 --- a/bin/tests/common/mod.rs +++ b/bin/tests/common/mod.rs @@ -1,5 +1,6 @@ use std::{collections::HashSet, path::Path}; +#[allow(dead_code)] pub trait AssertDirContents { fn assert_dir_contents(&self, expected_contents: &[&str]); } diff --git a/bin/tests/format_test.rs b/bin/tests/format_test.rs index 50f00666..66370666 100644 --- a/bin/tests/format_test.rs +++ b/bin/tests/format_test.rs @@ -46,3 +46,73 @@ fn root_dir_arg() -> Result<(), Box> { Ok(()) } + +#[test] +fn glob_arg() -> Result<(), Box> { + let root_dir = TempDir::new()?; + let file_a = root_dir.child("src/file_a.kn"); + let file_b = root_dir.child("src/file_b.kn"); + let file_c = root_dir.child("src/file_c.kn"); + file_a.write_str(INPUT)?; + file_b.write_str(INPUT)?; + file_c.write_str(INPUT)?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("format"); + cmd.arg("**/*_a.kn"); + + cmd.assert().success(); + + file_a.assert(OUTPUT); + file_b.assert(INPUT); + file_c.assert(INPUT); + + root_dir.close()?; + + Ok(()) +} + +#[test] +fn multiple() -> Result<(), Box> { + let root_dir = TempDir::new()?; + let file_a = root_dir.child("src/file_a.kn"); + let file_b = root_dir.child("src/file_b.kn"); + let file_c = root_dir.child("src/file_c.kn"); + file_a.write_str(INPUT)?; + file_b.write_str(INPUT)?; + file_c.write_str(INPUT)?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("format"); + + cmd.assert().success(); + + file_a.assert(OUTPUT); + file_b.assert(OUTPUT); + file_c.assert(OUTPUT); + + root_dir.close()?; + + Ok(()) +} + +#[test] +fn ignore_semantics() -> Result<(), Box> { + let root_dir = TempDir::new()?; + let entry = root_dir.child("src/main.kn"); + entry.write_str(" const \n FOO = BAR\n ; ")?; + + let mut cmd = Command::cargo_bin("knot")?; + cmd.current_dir(&root_dir); + cmd.arg("format"); + + cmd.assert().success(); + + entry.assert("const FOO = BAR;\n"); + + root_dir.close()?; + + Ok(()) +} diff --git a/command/Cargo.toml b/command/Cargo.toml index 81938b99..3fd85231 100644 --- a/command/Cargo.toml +++ b/command/Cargo.toml @@ -16,3 +16,4 @@ glob = "0.3.1" stdext = "0.3.3" kore = { package = "knot_core", path = "../kore", features = ["test"] } js = { package = "knot_plugin_javascript", path = "../plugin_javascript" } +engine = { package = "knot_engine", path = "../engine", features = ["test"] } diff --git a/command/src/assertions.rs b/command/src/assertions.rs new file mode 100644 index 00000000..5d2d9065 --- /dev/null +++ b/command/src/assertions.rs @@ -0,0 +1,44 @@ +use engine::{ConfigurationError, Report, Result}; +use std::{ + fs, + path::{Path, PathBuf}, +}; + +pub trait AssertExists: Sized { + fn assert_dir_exists(self, factory: F) -> Result + where + F: Fn(PathBuf) -> ConfigurationError; + + fn assert_file_exists(self, factory: F) -> Result + where + F: Fn(PathBuf) -> ConfigurationError; +} + +impl AssertExists for T +where + T: AsRef, +{ + fn assert_dir_exists(self, factory: F) -> Result + where + F: Fn(PathBuf) -> ConfigurationError, + { + match fs::metadata(&self) { + Ok(meta) if meta.is_dir() => Ok(self), + _ => Err(Box::new(Report::Configuration(factory( + self.as_ref().to_path_buf(), + )))), + } + } + + fn assert_file_exists(self, factory: F) -> Result + where + F: Fn(PathBuf) -> ConfigurationError, + { + match fs::metadata(&self) { + Ok(meta) if meta.is_file() => Ok(self), + _ => Err(Box::new(Report::Configuration(factory( + self.as_ref().to_path_buf(), + )))), + } + } +} diff --git a/command/src/build.rs b/command/src/build.rs index 91dd90d7..786c63b8 100644 --- a/command/src/build.rs +++ b/command/src/build.rs @@ -1,5 +1,5 @@ use crate::log; -use engine::{Context, Engine, FileSystem, Reporter}; +use engine::Engine; use kore::{color::Highlight, pretty::Pretty, Generator}; use lang::ast; use std::path::Path; @@ -18,12 +18,9 @@ pub fn command(opts: &Options) -> engine::Result<()> where G: Generator, { - let resolver = FileSystem(opts.source_dir); - let engine = Engine::new(Context::std(Reporter::new(false), resolver)); - log::entrypoint(opts.entry); - let count = engine + let count = Engine::new(opts.source_dir) .from_entry(opts.entry) .parse_and_discover() .inspect(|state, _| log::parsed_from_entry(state.internal_modules().count())) @@ -34,6 +31,8 @@ where .generate(&opts.generator) .overwrite(opts.out_dir)?; + eprintln!(); + log::success("transpiled", count); eprintln!( "build artifacts written to {}:\n{}\n", diff --git a/command/src/check.rs b/command/src/check.rs index b7110cca..ed7e5ca6 100644 --- a/command/src/check.rs +++ b/command/src/check.rs @@ -1,5 +1,5 @@ use crate::log; -use engine::{Context, Engine, FileSystem, Reporter}; +use engine::Engine; use std::path::Path; pub struct Options<'a> { @@ -8,12 +8,9 @@ pub struct Options<'a> { } pub fn command(opts: &Options) -> engine::Result<()> { - let resolver = FileSystem(opts.source_dir); - let engine = Engine::new(Context::std(Reporter::new(false), resolver)); - log::entrypoint(opts.entry); - let result = engine + let result = Engine::new(opts.source_dir) .from_entry(opts.entry) .parse_and_discover() .inspect(|state, _| log::parsed_from_entry(state.internal_modules().count())) @@ -22,7 +19,9 @@ pub fn command(opts: &Options) -> engine::Result<()> { .analyze() .into_result()?; - log::success("analyzed", result.graph.size()); + eprintln!(); + + log::success("analyzed", result.1.size()); Ok(()) } diff --git a/command/src/format.rs b/command/src/format.rs index fc035e6a..8c2def87 100644 --- a/command/src/format.rs +++ b/command/src/format.rs @@ -1,5 +1,5 @@ -use crate::{log, path::AssertExists}; -use engine::{Context, Engine, FileSystem, Reporter}; +use crate::{log, AssertExists}; +use engine::{ConfigurationError, Engine}; use std::path::Path; pub struct Options<'a> { @@ -12,21 +12,21 @@ pub struct Options<'a> { } pub fn command(opts: &Options) -> engine::Result<()> { - let resolver = FileSystem( - opts.root_dir - .assert_dir_exists(engine::Error::RootDirectoryNotFound)?, - ); - let engine = Engine::new(Context::std(Reporter::new(false), resolver)); + let root_dir = opts + .root_dir + .assert_dir_exists(ConfigurationError::RootDirectoryNotFound)?; log::glob(opts.glob); - let count = engine + let count = Engine::new(root_dir) .from_glob(opts.root_dir, opts.glob) - .parse_matched() + .parse_all() .inspect(|state, _| log::parsed_from_glob(state.internal_modules().count())) .format() .write(opts.root_dir)?; + eprintln!(); + log::success("formatted", count); Ok(()) diff --git a/command/src/lib.rs b/command/src/lib.rs index 13022511..cdc6ed52 100644 --- a/command/src/lib.rs +++ b/command/src/lib.rs @@ -1,8 +1,9 @@ +mod assertions; pub mod build; pub mod check; pub mod format; mod log; -mod path; +mod phase; -pub use engine::Error; -pub use lang::ast; +pub use assertions::AssertExists; +pub use phase::Phase; diff --git a/command/src/log.rs b/command/src/log.rs index 558b1e31..fc27e393 100644 --- a/command/src/log.rs +++ b/command/src/log.rs @@ -36,7 +36,7 @@ pub fn analyzed() { pub fn success(status: &str, count: usize) { eprintln!( - "\n{} {} {}\n", + "{} {} {}\n", status.success(), count.to_string().focus(), "module(s) with no errors".success() diff --git a/command/src/path.rs b/command/src/path.rs deleted file mode 100644 index 26592171..00000000 --- a/command/src/path.rs +++ /dev/null @@ -1,37 +0,0 @@ -use engine::{Error, Result}; -use std::{ - fs, - path::{Path, PathBuf}, -}; - -pub trait AssertExists: Sized { - fn assert_dir_exists(self, factory: F) -> Result - where - F: Fn(PathBuf) -> Error; - - fn assert_file_exists(self, factory: F) -> Result - where - F: Fn(PathBuf) -> Error; -} - -impl<'a> AssertExists for &'a Path { - fn assert_dir_exists(self, factory: F) -> Result - where - F: Fn(PathBuf) -> Error, - { - match fs::metadata(self) { - Ok(meta) if meta.is_dir() => Ok(self), - _ => Err(vec![factory(self.to_path_buf())]), - } - } - - fn assert_file_exists(self, factory: F) -> Result - where - F: Fn(PathBuf) -> Error, - { - match fs::metadata(self) { - Ok(meta) if meta.is_file() => Ok(self), - _ => Err(vec![factory(self.to_path_buf())]), - } - } -} diff --git a/command/src/phase.rs b/command/src/phase.rs new file mode 100644 index 00000000..2302e29c --- /dev/null +++ b/command/src/phase.rs @@ -0,0 +1,28 @@ +use kore::color::Highlight; +use std::fmt::Display; + +pub enum Phase { + Configuration, + Execution, + Report, + Result, +} + +impl Display for Phase { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + writeln!( + f, + "{}\n", + format!( + "[ {} ]", + match self { + Self::Configuration => "configuration", + Self::Execution => "execution", + Self::Report => "report", + Self::Result => "result", + } + ) + .subtle() + ) + } +} diff --git a/command/tests/build_all_type_expressions_test.rs b/command/tests/build_all_type_expressions_test.rs index d71f9402..4d71431c 100644 --- a/command/tests/build_all_type_expressions_test.rs +++ b/command/tests/build_all_type_expressions_test.rs @@ -2,13 +2,16 @@ mod common; use common::assert_build_single; +use std::fmt::Write; fn assert_build_type_expressions(name: &str, inputs: &[&str]) { let source = inputs .iter() .enumerate() - .map(|(index, input)| format!("type TYPE_{index} = {input};")) - .collect::(); + .fold(String::new(), |mut acc, (index, input)| { + write!(&mut acc, "type TYPE_{index} = {input};").ok(); + acc + }); let compiled = "import { $knot } from \"@knot/runtime\";\n"; diff --git a/command/tests/build_import_errors_test.rs b/command/tests/build_import_errors_test.rs index 7b68cee0..0459e0e0 100644 --- a/command/tests/build_import_errors_test.rs +++ b/command/tests/build_import_errors_test.rs @@ -17,11 +17,11 @@ fn cyclic() { ); assert_eq!( - result, - Err(vec![engine::Error::ImportCycle(vec![ + result.unwrap_err().exec_errors().unwrap(), + &vec![engine::ExecutionError::ImportCycle(vec![ Link::from("b"), Link::from("a"), Link::from("c"), - ])]) + ])] ); } diff --git a/command/tests/common/mod.rs b/command/tests/common/mod.rs index e4cc6ec5..8e4f4ab6 100644 --- a/command/tests/common/mod.rs +++ b/command/tests/common/mod.rs @@ -1,4 +1,4 @@ -#![allow(dead_code, clippy::expect_used, clippy::create_dir)] +#![allow(dead_code, unused_imports, clippy::expect_used, clippy::create_dir)] mod build; mod format; diff --git a/engine/docs/architecture.d2 b/engine/docs/architecture.d2 new file mode 100644 index 00000000..20d0e739 --- /dev/null +++ b/engine/docs/architecture.d2 @@ -0,0 +1,58 @@ +entrypoint: Entrypoint +glob: Glob +engine: Engine +target_entrypoint: From Entrypoint +target_entrypoint.shape: oval +target_glob: From Glob +target_glob.shape: oval +format: Format +format.shape: oval +parse: Parse +parse.shape: oval +link_: Link +link_.shape: oval +analyze: Analyze +analyze.shape: oval +generate: Generate +generate.shape: oval +format_writer: Writer +transpile_writer: Writer + +context: Context { + resolver: Resolver + reporter: Reporter +} + +targeted: Engine { + state: Source +} + +parsed: Engine { + state: Parsed State +} + +linked: Engine { + state: Linked State +} + +analyzed: Engine { + state: Analyzed State +} + +context -> engine +entrypoint -> target_entrypoint +engine -> target_entrypoint +engine -> target_glob +glob -> target_glob +target_entrypoint -> targeted +target_glob -> targeted +targeted -> format +format -> format_writer +targeted -> parse +parse -> parsed +parsed -> link_ +link_ -> linked +linked -> analyze +analyze -> analyzed +analyzed -> generate +generate -> transpile_writer diff --git a/engine/src/lib.rs b/engine/src/lib.rs index c94d63fa..45b9bd83 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -7,22 +7,44 @@ mod validate; mod write; use analyze::ModuleMap; -use bimap::BiMap; use kore::{invariant, Generator, Incrementor}; -use lang::{ast, NamespaceId}; +use lang::{ast, Canonicalize, NamespaceId, NodeId}; pub use link::Link; -pub use report::{CodeFrame, Error, Reporter}; +use report::Enrich; +pub use report::{ + CodeFrame, ConfigurationError, EnvironmentError, ExecutionError, Report, Reporter, +}; pub use resolve::{FileCache, FileSystem, MemoryCache, Resolver}; pub use resource::Library; -use state::Modules; +use state::FromPaths; use std::{ collections::{HashMap, HashSet, VecDeque}, - path::Path, + env::current_dir, + fmt::Display, + ops::Deref, + path::{Path, PathBuf}, }; use validate::Validator; use write::Writer; -pub type Result = std::result::Result>; +pub type Result = std::result::Result>; + +/// internal result type used to propagate errors +type Internal = std::result::Result>; + +pub trait IntoResult { + type Value; + + fn into_result(self) -> Result; +} + +impl IntoResult for Result { + type Value = T; + + fn into_result(self) -> Result { + self + } +} pub struct Context { reporter: Reporter, @@ -38,12 +60,43 @@ impl Context { libraries: HashSet::from_iter(vec![Library::Std, Library::Html]), } } + + pub fn raise(&mut self, x: T) -> Internal<()> + where + T: report::IntoErrors, + { + self.reporter.raise(x) + } + + pub fn fail(&mut self, x: T) -> report::Failure + where + T: report::IntoErrors, + { + self.reporter.fail(x) + } } -pub struct Engine +impl Context where R: Resolver, { + pub fn load_and_parse_program(&mut self, link: &Link) -> Internal<(String, state::Ast<()>)> { + let path = link.to_path(); + + let input = self + .resolver + .resolve(&path) + .ok_or_else(|| self.fail(ExecutionError::ModuleNotFound(link.clone())))?; + + let (ast, _) = parse::program::parse(&input) + .map_err(|_| self.fail(ExecutionError::InvalidSyntax(link.clone())))?; + + Ok((input, state::Ast::Program(ast))) + } +} + +pub struct Engine { + root_dir: String, context: Context, state: T, } @@ -59,38 +112,12 @@ where let state = f(self.state, &mut self.context); Engine { + root_dir: self.root_dir, context: self.context, state, } } - fn to_links(link: &Link, ast: &state::Ast) -> Vec { - let path = link.to_path(); - - if let state::Ast::Program(program) = ast { - program - .imports() - .iter() - .map(|x| Link::from_import(&path, x.0.value())) - .collect::>() - } else { - vec![] - } - } - - fn load_and_parse_program(resolver: &mut R, link: &Link) -> Result<(String, state::Ast<()>)> { - let path = link.to_path(); - - let input = resolver - .resolve(&path) - .ok_or(vec![Error::ModuleNotFound(link.clone())])?; - - let (ast, _) = - parse::program::parse(&input).map_err(|_| vec![Error::InvalidSyntax(link.clone())])?; - - Ok((input, state::Ast::Program(ast))) - } - fn parse_library(library: &Library) -> (String, state::Ast<()>) { let input = library.resolve(); let (ast, _) = parse::typings::parse(input) @@ -119,20 +146,6 @@ impl Engine, R> where R: Resolver, { - pub fn then(self, f: F) -> Engine, R> - where - F: Fn(T, &mut Context) -> Result, - { - self.map(|state, context| match state { - Ok(state) => { - context.reporter.catch()?; - - f(state, context) - } - Err(err) => Err(err), - }) - } - pub fn inspect(self, f: F) -> Self where F: Fn(&T, &Context), @@ -144,19 +157,100 @@ where self } - pub fn into_result(self) -> Result { - self.state + fn to_writer(&self, f: F) -> Writer + where + T2: Display, + F: Fn(&T) -> Vec<(PathBuf, T2)>, + { + Writer(self.state.as_ref().map(f).map_err(std::clone::Clone::clone)) } } -impl Engine<(), R> +impl Engine +where + T: IntoResult, + R: Resolver, +{ + pub fn into_result(self) -> Result { + self.state.into_result() + } +} + +impl Engine +where + T: Clone + Enrich, + U: IntoResult, + R: Resolver, +{ + fn then(self, f: F) -> Engine, R> + where + F: Fn(T, &mut Context) -> Internal, + { + let try_apply = |state, context: &mut Context| -> Internal { + context.reporter.flush()?; + + let result = f(state, context)?; + + context.reporter.flush()?; + + Ok(result) + }; + + let root_dir = self.root_dir.clone(); + self.map(|result, context| { + let state = result.into_result()?; + + try_apply(state.clone(), context) + .map_err(|err| Box::new(state.enrich(root_dir.clone(), *err))) + }) + } +} + +impl Engine, R> where + S: Deref>, + T: Clone, R: Resolver, { - pub const fn new(context: Context) -> Self { - Self { context, state: () } + /// generate output files by formatting the loaded modules + pub fn format(&self) -> Writer> { + self.to_writer(|state| { + state + .modules() + .filter_map(|(link, state::Module { ast, .. })| match ast { + state::Ast::Program(x) if link.is_internal() => { + Some((link.to_path(), x.clone())) + } + + state::Ast::Program(_) | state::Ast::Typings(_) => None, + }) + .collect() + }) } +} + +impl<'a> Engine<(), FileSystem<'a>> { + pub fn new(root_dir: &'a Path) -> Self { + let context = Context::std(Reporter::new(false), FileSystem(root_dir)); + let relative_root = if let Ok(working_dir) = current_dir() { + root_dir.strip_prefix(working_dir).unwrap_or(root_dir) + } else { + root_dir + }; + + Self { + context, + root_dir: relative_root.to_string_lossy().to_string(), + state: (), + } + } +} + +impl Engine<(), R> +where + R: Resolver, +{ /// load a module tree from a single entry point pub fn from_entry(self, entry: &Path) -> Engine { assert!( @@ -168,210 +262,136 @@ where } /// load all modules that match a glob - pub fn from_glob<'a>(self, dir: &'a Path, glob: &'a str) -> Engine, R> { - self.map(|(), _| state::FromGlob { dir, glob }) + pub fn from_glob<'a>( + self, + dir: &'a Path, + glob: &'a str, + ) -> Engine, R> { + self.map(|(), _| state::FromGlob { dir, glob }.to_paths()) } } -impl Engine +impl Engine where + T: IntoResult, R: Resolver, { /// starting from the entry file recursively discover and parse modules pub fn parse_and_discover(self) -> Engine, R> { - self.map(|state, context| { - let mut incrementor = Incrementor::default(); + self.then(|state, context| { let mut queue = VecDeque::from_iter(vec![state.0]); - let mut ambient = HashMap::new(); - let mut modules = HashMap::new(); - let mut lookup = BiMap::new(); + let mut parsed = state::Parsed::default(); for (library, link, module) in - Self::parse_libraries(&mut incrementor, &context.libraries) + Self::parse_libraries(&mut parsed.incrementor().borrow_mut(), &context.libraries) { - if let Some(ambient_scope) = library.to_ambient_scope() { - ambient.insert(ambient_scope, module.id); - } - modules.insert(link, module); + parsed.register_library(library, link, module); } while let Some(link) = queue.pop_front() { - match Self::load_and_parse_program(&mut context.resolver, &link) { - Ok((input, ast)) => { - let links = Self::to_links(&link, &ast); - - for link in links { - if !modules.contains_key(&link) && !queue.contains(&link) { - queue.push_back(link); - } + context.load_and_parse_program(&link).map(|(text, ast)| { + for link in ast.to_links(&link) { + if !parsed.has_by_link(&link) && !queue.contains(&link) { + queue.push_back(link); } - - let namespace_id = NamespaceId(incrementor.increment()); - lookup.insert(link.clone(), namespace_id); - modules.insert(link, state::Module::new(namespace_id, input, ast)); } - Err(errs) => context.reporter.raise(errs)?, - } + parsed.register_source(link, text, ast); + })?; } - context.reporter.catch()?; - - Ok(state::Parsed { - modules, - lookup, - ambient, - }) + Ok(parsed) }) } } -impl<'a, R> Engine, R> +impl Engine where + T: IntoResult, R: Resolver, { - /// parse all modules that match the glob - pub fn parse_matched(self) -> Engine, R> { - self.map(move |state, context| { - let links = state - .to_paths() - .map_err(|errs| vec![Error::InvalidGlob(errs)])? - .iter() - .map(Link::from) - .collect::>(); - let mut incrementor = Incrementor::default(); - let mut modules = HashMap::new(); - let mut ambient = HashMap::new(); - let mut lookup = BiMap::new(); + /// parse all modules from the provided paths + pub fn parse_all(self) -> Engine, R> { + self.then(|FromPaths(links), context| { + let mut parsed = state::Parsed::default(); for (library, link, module) in - Self::parse_libraries(&mut incrementor, &context.libraries) + Self::parse_libraries(&mut parsed.incrementor().borrow_mut(), &context.libraries) { - if let Some(ambient_scope) = library.to_ambient_scope() { - ambient.insert(ambient_scope, module.id); - } - modules.insert(link, module); + parsed.register_library(library, link, module); } for link in links { - match Self::load_and_parse_program(&mut context.resolver, &link) { - Ok((text, ast)) => { - let namespace_id = NamespaceId(incrementor.increment()); - lookup.insert(link.clone(), namespace_id); - modules.insert(link, state::Module::new(namespace_id, text, ast)); - } - - Err(errs) => context.reporter.raise(errs)?, - } + context + .load_and_parse_program(&link) + .map(|(text, ast)| parsed.register_source(link, text, ast))?; } - context.reporter.catch()?; - - Ok(state::Parsed { - modules, - lookup, - ambient, - }) + Ok(parsed) }) } } -impl<'a, S, R> Engine -where - S: state::Modules<'a>, - S::Meta: Clone, - R: Resolver, -{ - /// generate output files by formatting the loaded modules - pub fn format(&'a self) -> Writer<&ast::meta::Program> { - Writer(self.state.modules().map(|modules| { - modules - .filter_map(|(link, state::Module { ast, .. })| match ast { - state::Ast::Program(x) if link.is_internal() => Some((link.to_path(), x)), - - state::Ast::Program(_) | state::Ast::Typings(_) => None, - }) - .collect::>() - })) - } -} - -impl Engine, R> +impl Engine where + T: IntoResult, R: Resolver, { pub fn link(self) -> Engine, R> { self.then(|state, context| { - let linked = state.internal_modules().fold( - state.to_import_graph(), - |mut acc, (link, module)| { - let links = Self::to_links(link, &module.ast); - - for x in &links { - if let Some(x) = state.lookup.get_by_left(x) { - acc.add_edge(&module.id, x).ok(); - } else { - context - .reporter - .report(Error::UnregisteredModule(x.clone())); - } - } - - acc - }, - ); - - context.reporter.catch_early()?; + let linked = state.link_modules(context)?; - let validator = Validator(&state); - context - .reporter - .raise(validator.assert_no_import_cycles(&linked))?; + Validator(context).validate(&state, &linked)?; Ok(state::Linked::new(state, linked)) }) } } -impl Engine, R> +impl Engine where + T: IntoResult, R: Resolver, { fn get_module<'a>( state: &'a state::Linked, id: &'a NamespaceId, ) -> (&'a Link, &'a state::Module<()>) { - let link = state.lookup.get_by_right(id).unwrap_or_else(|| { + state.get_link_and_module_by_id(id).unwrap_or_else(|| { invariant!("did not find link for module with id {id} in state lookup") - }); + }) + } - ( - link, - state - .get_module(link) - .unwrap_or_else(|| invariant!("did not find module at link {link}")), - ) + fn bind_errors( + context: &analyze::Context, + errors: Vec<(NodeId, analyze::Error)>, + ) -> Vec { + errors + .into_iter() + .map(|(id, err)| ExecutionError::AnalysisError(context.canonicalize(id), err)) + .collect() } pub fn analyze(self) -> Engine, R> { - self.then(|state, _| { + self.then(|state, context| { let mut analyzed = HashMap::default(); let mut modules = ModuleMap::default(); // TODO: abstract this so the same logic can be re-used between both libraries and source modules - for (link, module) in state.modules()?.filter(|(link, _)| link.is_library()) { + for (link, module) in state.modules().filter(|(link, _)| link.is_library()) { let namespace = link.clone().to_namespace(); - let context = analyze::Context { + let analyze_context = analyze::Context { id: module.id, namespace: &namespace, modules: &modules, - ambient: &state.ambient, + ambient: state.ambient(), }; + // TODO: see if it's possible to fail after all libraries are processed instead of immediately let (typed, types) = module .ast - .analyze(&context) - .unwrap_or_else(|errs| unimplemented!("need to handle errors:\n{errs:?}")); + .analyze(&analyze_context) + .map_err(|errs| context.fail(Self::bind_errors(&analyze_context, errs)))?; modules .by_key @@ -382,20 +402,21 @@ where ); } - for id in state.graph.iter() { + for id in state.iter_graph() { let (link, module) = Self::get_module(&state, &id); let namespace = link.clone().to_namespace(); - let context = analyze::Context { + let analyze_context = analyze::Context { id: module.id, namespace: &namespace, modules: &modules, - ambient: &state.ambient, + ambient: state.ambient(), }; + // TODO: see if it's possible to fail after all modules are processed instead of immediately let (typed, types) = module .ast - .analyze(&context) - .unwrap_or_else(|errs| unimplemented!("need to handle errors:\n{errs:?}")); + .analyze(&analyze_context) + .map_err(|errs| context.fail(Self::bind_errors(&analyze_context, errs)))?; modules.keys.insert(namespace, module.id); modules @@ -421,8 +442,8 @@ where where T: Generator, { - Writer(match &self.state { - Ok(state) => Ok(state + self.to_writer(|state| { + state .internal_modules() .filter_map(|(key, state::Module { ast, .. })| { if let state::Ast::Program(program) = ast { @@ -431,9 +452,7 @@ where None } }) - .collect()), - - Err(err) => Err(err.clone()), + .collect() }) } } diff --git a/engine/src/link/import_graph/mod.rs b/engine/src/link/import_graph/mod.rs index 6e7bea22..56c5290f 100644 --- a/engine/src/link/import_graph/mod.rs +++ b/engine/src/link/import_graph/mod.rs @@ -46,6 +46,7 @@ impl Hash for Cycle { } } +#[derive(Clone)] pub struct ImportGraph { graph: StableDiGraph, lookup: BiMap, diff --git a/engine/src/link/mod.rs b/engine/src/link/mod.rs index f1dea395..3dc4554d 100644 --- a/engine/src/link/mod.rs +++ b/engine/src/link/mod.rs @@ -5,7 +5,7 @@ use kore::str; use lang::{ast, Namespace, NamespaceKind}; use std::{ ffi::OsStr, - fmt::{Debug, Display, Formatter}, + fmt::{Debug, Display}, path::{Path, PathBuf}, }; @@ -15,6 +15,11 @@ use crate::Library; pub struct Link(Namespace); impl Link { + #[cfg(feature = "test")] + pub fn mock() -> Self { + Self(Namespace::mock()) + } + pub fn from_import

(file_path: P, import: &ast::Import) -> Self where P: AsRef, @@ -69,7 +74,7 @@ where } impl Display for Link { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - self.to_path().fmt(f) + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + std::fmt::Display::fmt(&self.to_path().display(), f) } } diff --git a/engine/src/report/code_frame.rs b/engine/src/report/code_frame.rs index 99b0c6fc..4efb6df7 100644 --- a/engine/src/report/code_frame.rs +++ b/engine/src/report/code_frame.rs @@ -1,18 +1,252 @@ use crate::Link; +use kore::color::{ClearIf, Colorize, Highlight}; use lang::Range; +use std::fmt::Display; + +const BORDER: &str = "\u{2502}"; +const CORNER: &str = "\u{256d}\u{2500}"; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Focus { + Error, + Success, + Highlight, +} #[derive(Clone, Debug, Eq, PartialEq)] -pub struct CodeFrame { - link: Link, - range: Range, +pub struct CodeFrame<'a> { + pub root_dir: &'a str, + pub link: &'a Link, + pub source: &'a str, + pub color: bool, + pub focus: Focus, + pub range: Range, + pub padding: usize, } -impl CodeFrame { - pub const fn new(link: Link, range: Range) -> Self { - Self { link, range } +impl<'a> CodeFrame<'a> { + pub const fn link(&self) -> &Link { + self.link } - pub const fn link(&self) -> &Link { - &self.link + fn get_lines(&self) -> Lines { + let Range(start, end) = self.range; + + let sample_start = start.0.checked_sub(self.padding).unwrap_or_default(); + let sample_end = end.0 + self.padding + 1; + + let mut lines = vec![]; + let mut gutter = 1; + let mut last_row = end.0; + + for row in sample_start..sample_end { + if let Some(line) = row + .checked_sub(1) + .and_then(|index| self.source.lines().nth(index)) + { + // ignore preceding empty lines + if lines.is_empty() && line.is_empty() { + continue; + } + + lines.push((row, line)); + gutter = row.to_string().len().max(gutter); + last_row = row; + } + } + + // trim trailing empty lines + while let Some((_, line)) = lines.last() { + if line.is_empty() { + lines.pop(); + last_row -= 1; + } else { + break; + } + } + + Lines { + gutter, + last_row, + lines, + } + } + + fn format_header(&self, gutter_width: usize, no_color: bool) -> String { + let gutter = " ".repeat(gutter_width); + let link = self.link(); + + format!( + "{gutter}{} {} {}\n{}\n", + CORNER.subtle().clear_if(no_color), + link.to_string().highlight().clear_if(no_color), + format!( + "({root_dir}/{link}:{point})", + root_dir = self.root_dir, + point = self.range.0 + ) + .subtle() + .clear_if(no_color), + format!("{gutter}{BORDER}").subtle().clear_if(no_color) + ) + } +} + +impl<'a> Display for CodeFrame<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn format_caret( + gutter_width: usize, + line_width: usize, + row: usize, + focus: Focus, + Range(start, end): Range, + no_color: bool, + ) -> String { + let is_wrapping = start.0 != end.0; + let mut caret = String::new(); + + let mut fill_range = |from, to| { + for _ in 0..from { + caret.push(' '); + } + for _ in from..to { + match focus { + Focus::Error => caret.push('^'), + Focus::Success => caret.push('~'), + Focus::Highlight => caret.push('-'), + } + } + }; + + if is_wrapping { + if row == start.0 { + fill_range(start.1 - 1, line_width); + } else if row < end.0 { + fill_range(0, line_width); + } else if row == end.0 { + fill_range(0, end.1); + } + } else { + fill_range(start.1 - 1, end.1); + } + + format!( + "{}{} {}", + " ".repeat(gutter_width), + BORDER.subtle().clear_if(no_color), + match focus { + Focus::Error => caret.error(), + Focus::Success => caret.success(), + Focus::Highlight => caret.highlight(), + } + .clear_if(no_color) + ) + } + + fn format_line(gutter_width: usize, line: usize, code: &str, no_color: bool) -> String { + format!( + "{:>gutter_width$}{} {}", + line.to_string().subtle().clear_if(no_color), + BORDER.subtle().clear_if(no_color), + code.dimmed().clear_if(no_color), + ) + } + + let no_color = !self.color; + let Lines { + gutter, + last_row, + lines, + } = self.get_lines(); + + self.format_header(gutter, no_color).fmt(f)?; + + for (row, line) in lines { + format_line(gutter, row, line, no_color).fmt(f)?; + + if row >= self.range.0 .0 && row <= self.range.1 .0 { + write!( + f, + "\n{}", + format_caret(gutter, line.len(), row, self.focus, self.range, no_color) + )?; + } + + if row != last_row { + writeln!(f)?; + } + } + + Ok(()) + } +} + +struct Lines<'a> { + gutter: usize, + last_row: usize, + lines: Vec<(usize, &'a str)>, +} + +#[cfg(test)] +mod tests { + use super::{CodeFrame, Focus}; + use crate::Link; + use kore::assert_str_eq; + use lang::Range; + + #[test] + fn highlight_range() { + let link = Link::mock(); + let source = "const FOO = 123; +const BAR = FOO + 10; +type Fizz = integer; +type Buzz = boolean;"; + + assert_str_eq!( + CodeFrame { + root_dir: "./src", + link: &link, + source, + range: Range::new((2, 13), (2, 15)), + focus: Focus::Error, + padding: 2, + color: false + } + .to_string(), + " \u{256d}\u{2500} mock.kn (./src/mock.kn:2:13) + \u{2502} +1\u{2502} const FOO = 123; +2\u{2502} const BAR = FOO + 10; + \u{2502} ^^^ +3\u{2502} type Fizz = integer; +4\u{2502} type Buzz = boolean;" + ); + } + + #[test] + fn trim_empty_lines() { + let link = Link::mock(); + let source = " + +const FOO = 123; + +"; + + assert_str_eq!( + CodeFrame { + root_dir: "./src", + link: &link, + source, + range: Range::new((3, 13), (3, 15)), + focus: Focus::Error, + padding: 2, + color: false + } + .to_string(), + " \u{256d}\u{2500} mock.kn (./src/mock.kn:3:13) + \u{2502} +3\u{2502} const FOO = 123; + \u{2502} ^^^" + ); } } diff --git a/engine/src/report/error.rs b/engine/src/report/error.rs deleted file mode 100644 index 2dc2fe1a..00000000 --- a/engine/src/report/error.rs +++ /dev/null @@ -1,212 +0,0 @@ -use crate::Link; -use kore::{ - color::{Colorize, Highlight}, - format::SeparateEach, - pretty::Pretty, -}; -use std::{fmt::Display, io, path::PathBuf}; - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum Error { - // internal errors - UnregisteredModule(Link), - - // environment errors - InvalidWriteTarget(PathBuf, io::ErrorKind), - InvalidGlob(Vec), - RootDirectoryNotFound(PathBuf), - SourceDirectoryNotFound(PathBuf), - SourceDirectoryNotRelative(PathBuf), - EntrypointNotFound(PathBuf), - EntrypointNotRelative(PathBuf), - CleanupFailed(PathBuf), - - // parsing errors - InvalidSyntax(Link), - - // linking errors - ModuleNotFound(Link), - ImportCycle(Vec), - // analysis errors - - // generation errors -} - -impl Error { - pub const fn code(&self) -> ErrorCode { - match self { - // internal errors - Self::UnregisteredModule(..) => ErrorCode::UNREGISTERED_MODULE, - - // environment errors - Self::InvalidWriteTarget(.., io::ErrorKind::NotFound) => { - ErrorCode::INVALID_WRITE_TARGET_NOT_FOUND - } - Self::InvalidWriteTarget(.., io::ErrorKind::PermissionDenied) => { - ErrorCode::INVALID_WRITE_TARGET_PERMISSION_DENIED - } - Self::InvalidWriteTarget(..) => ErrorCode::INVALID_WRITE_TARGET, - Self::InvalidGlob(..) => ErrorCode::INVALID_GLOB, - Self::RootDirectoryNotFound(..) => ErrorCode::ROOT_DIRECTORY_NOT_FOUND, - Self::SourceDirectoryNotFound(..) => ErrorCode::SOURCE_DIRECTORY_NOT_FOUND, - Self::SourceDirectoryNotRelative(..) => ErrorCode::SOURCE_DIRECTORY_NOT_RELATIVE, - Self::EntrypointNotFound(..) => ErrorCode::ENTRYPOINT_NOT_FOUND, - Self::EntrypointNotRelative(..) => ErrorCode::ENTRYPOINT_NOT_RELATIVE, - Self::CleanupFailed(..) => ErrorCode::CLEANUP_FAILED, - - // parsing errors - Self::InvalidSyntax(..) => ErrorCode::INVALID_SYNTAX, - - // linking errors - Self::ModuleNotFound(..) => ErrorCode::MODULE_NOT_FOUND, - Self::ImportCycle(..) => ErrorCode::IMPORT_CYCLE, - } - } -} - -impl Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let code = self.code(); - - let (title, description) = match self { - // internal errors - Self::UnregisteredModule(..) => ( - "Unregistered Module", - format!( - "a referenced module was not found when linking\n\n{}", - "(this should not be possible and represents a fatal internal error)".error() - ), - ), - - // environment errors - Self::InvalidWriteTarget(path, error) => ( - "Invalid Write Target", - format!( - "attempted write to {} failed with error {error}", - path.pretty() - ), - ), - - Self::InvalidGlob(glob) => ( - "Invalid Glob", - format!( - "the provided glob failed to resolve with error(s):\n\n{errors}", - errors = SeparateEach( - "\n", - &glob - .iter() - .map(|x| format!("\u{2022} {x}")) - .collect::>() - ) - ), - ), - - Self::RootDirectoryNotFound(path) => ( - "Root Directory Not Found", - format!("no folder was found at the path {}", path.pretty()), - ), - - Self::SourceDirectoryNotFound(path) => ( - "Source Directory Not Found", - format!("no folder was found at the path {}", path.pretty()), - ), - - Self::SourceDirectoryNotRelative(path) => ( - "Source Directory Not Relative", - format!( - "the path to the source directory should be relative to the {} but found {}", - "root_dir".highlight(), - path.pretty().error() - ), - ), - - Self::EntrypointNotFound(path) => ( - "Entrypoint Not Found", - format!("no module was found at the path {}", path.pretty()), - ), - - Self::EntrypointNotRelative(path) => ( - "Entrypoint Not Relative", - format!( - "the path to the entrypoint should be relative to the {} but found {}", - "source_dir".highlight(), - path.pretty().error() - ), - ), - - Self::CleanupFailed(path) => ( - "Cleanup Failed", - format!("unable to delete {} or its contents", path.pretty()), - ), - - // parsing errors - Self::InvalidSyntax(link) => ( - "Invalid Syntax", - format!( - "the file {} does not appear to contain valid Knot code", - link.to_path().pretty() - ), - ), - - // linking errors - Self::ModuleNotFound(link) => ( - "Module Not Found", - format!("unable to find module {}", link.to_path().pretty()), - ), - - Self::ImportCycle(links) => ( - "Import Cycle", - format!( - "an import cycle was found between the following modules:\n\n{}", - SeparateEach( - &format!(" {} ", "->".subtle()), - &links.iter().map(|x| x.to_path().pretty()).collect() - ) - ), - ), - }; - - write!( - f, - "{title} {code}\n\n{description}", - title = title.error().bold(), - code = format!("(E#{})", code.0).subtle(), - ) - } -} - -#[derive(Debug)] -pub struct ErrorCode(u16); - -impl ErrorCode { - // 0xx - internal errors - - pub const UNREGISTERED_MODULE: Self = Self(000); - - // 1xx - environment errors - - pub const INVALID_WRITE_TARGET: Self = Self(100); - pub const INVALID_WRITE_TARGET_NOT_FOUND: Self = Self(101); - pub const INVALID_WRITE_TARGET_PERMISSION_DENIED: Self = Self(102); - // [103..109] reserved space for additional writing errors - pub const INVALID_GLOB: Self = Self(110); - pub const ROOT_DIRECTORY_NOT_FOUND: Self = Self(111); - pub const SOURCE_DIRECTORY_NOT_FOUND: Self = Self(112); - pub const SOURCE_DIRECTORY_NOT_RELATIVE: Self = Self(113); - pub const ENTRYPOINT_NOT_FOUND: Self = Self(114); - pub const ENTRYPOINT_NOT_RELATIVE: Self = Self(115); - pub const CLEANUP_FAILED: Self = Self(116); - - // 2xx - parsing errors - - pub const INVALID_SYNTAX: Self = Self(200); - - // 3xx - linking errors - - pub const MODULE_NOT_FOUND: Self = Self(300); - pub const IMPORT_CYCLE: Self = Self(301); - - // 4xx - analysis errors - - // 5xx - generation errors -} diff --git a/engine/src/report/error/analysis.rs b/engine/src/report/error/analysis.rs new file mode 100644 index 00000000..baf23f95 --- /dev/null +++ b/engine/src/report/error/analysis.rs @@ -0,0 +1,579 @@ +use super::{ + code::ToCode, CodeFrame, Display, ErrorCode, ErrorContext, ErrorDisplay, ErrorDisplayBuilder, +}; +use crate::report::{example, Focus}; +use kore::{ + color::{ColoredString, Colorize, Highlight}, + format::SeparateEach, + invariant, +}; +use lang::{ast, types}; + +impl ToCode for analyze::Error { + fn to_code(&self) -> ErrorCode { + match self { + Self::NotInferrable(..) => ErrorCode::NOT_INFERRABLE, + Self::NotFound(..) => ErrorCode::NOT_FOUND, + Self::VariantNotFound(..) => ErrorCode::VARIANT_NOT_FOUND, + Self::DeclarationNotFound(..) => ErrorCode::DECLARATION_NOT_FOUND, + Self::NotIndexable(..) => ErrorCode::NOT_INDEXABLE, + Self::PropertyNotFound(..) => ErrorCode::PROPERTY_NOT_FOUND, + Self::DuplicateProperty(..) => ErrorCode::DUPLICATE_PROPERTY, + Self::NotSpreadable(..) => ErrorCode::NOT_SPREADABLE, + Self::UntypedParameter(..) => ErrorCode::UNTYPED_PARAMETER, + Self::DefaultValueRejected(..) => ErrorCode::DEFAULT_VALUE_REJECTED, + Self::NotCallable(..) => ErrorCode::NOT_CALLABLE, + Self::UnexpectedArgument(..) => ErrorCode::UNEXPECTED_ARGUMENT, + Self::MissingArgument(..) => ErrorCode::MISSING_ARGUMENT, + Self::ArgumentRejected(..) => ErrorCode::ARGUMENT_REJECTED, + Self::NotRenderable(..) => ErrorCode::NOT_RENDERABLE, + Self::InvalidComponent(..) => ErrorCode::INVALID_COMPONENT, + Self::ComponentTypo(..) => ErrorCode::COMPONENT_TYPO, + Self::InvalidAttributes(..) => ErrorCode::INVALID_ATTRIBUTES, + Self::UnexpectedAttribute(..) => ErrorCode::UNEXPECTED_ATTRIBUTE, + Self::MissingAttribute(..) => ErrorCode::MISSING_ATTRIBUTE, + Self::AttributeRejected(..) => ErrorCode::ATTRIBUTE_REJECTED, + Self::BinaryOperationNotSupported(..) => ErrorCode::BINARY_OPERATION_NOT_SUPPORTED, + Self::UnaryOperationNotSupported(..) => ErrorCode::UNARY_OPERATION_NOT_SUPPORTED, + Self::UnexpectedKind(..) => ErrorCode::UNEXPECTED_KIND, + } + } +} + +impl<'a> Display<'a> for analyze::Error { + type Context = (&'a lang::CanonicalId, &'a ErrorContext); + + fn display( + &'a self, + ( + id, + ErrorContext { + root_dir, + modules, + nodes, + }, + ): Self::Context, + ) -> ErrorDisplay<'a> { + fn format_list( + items: U, + ) -> SeparateEach> + where + T: AsRef, + U: IntoIterator, + { + let success_items = items + .into_iter() + .map(|x| x.as_ref().success()) + .collect::>(); + + SeparateEach( + ", ".subtle(), + if success_items.is_empty() { + vec!["(None)".subtle()] + } else { + success_items + }, + ) + } + + let code_frame = |focus, id| { + let range = *nodes + .get(id) + .unwrap_or_else(|| invariant!("node {id:?} does not exist in error context")); + let (link, text) = modules + .get(&id.0) + .unwrap_or_else(|| invariant!("module {:?} does not exist in error context", id.0)); + + CodeFrame { + root_dir, + link, + source: text, + focus, + range, + padding: 0, + color: true, + } + }; + + let error = ErrorDisplayBuilder::default() + .code(self.to_code()) + .code_frame(code_frame(Focus::Error, id)); + + match self { + Self::NotInferrable(references) => { + let mut builder = error.title("Not Inferrable").description( + "The type of this expression could not be inferred from other types.", + ); + + for reference_id in references { + builder = builder.reference( + format!( + "This {} is an unresolved dependency.", + "expression".highlight() + ), + code_frame(Focus::Highlight, reference_id), + ); + } + + builder + } + + Self::NotFound(name) => error + .title("Not Found") + .description(format!( + "This expression references a variable named {} which does not exist.", + name.error() + )) + .suggestion("Check the spelling of the variable name or declare a new variable.") + .example(example::local_variables(name)), + + Self::VariantNotFound(enum_id, valid_variants, expected_variant) => error + .title("Variant Not Found") + .description(format!( + "There is no variant named {name} declared on this enumerated type.", + name = expected_variant.error() + )) + .reference( + format!("The {} is referenced here.", "enumerated type".highlight()), + code_frame(Focus::Highlight, enum_id), + ) + .suggestion(format!( + "Declare a new variant or select an existing variant. + + {} {}", + "variants:".subtle(), + format_list(valid_variants) + )) + .example(example::enumerated_type_variants("First", expected_variant)), + + Self::DeclarationNotFound(module_id, valid_entities, expected_entity) => error + .title("Declaration Not Found") + .description(format!( + "There are no entities named {name} declared in this module.", + name = expected_entity.error() + )) + .reference( + format!("The {} is referenced here.", "module".highlight()), + code_frame(Focus::Highlight, module_id), + ) + .suggestion(format!( + "Declare a new entity or select an existing entity. + + {} {}", + "entities:".subtle(), + format_list(valid_entities) + )) + .example(example::constants(expected_entity)), + + Self::NotIndexable(value_id, name) => error + .title("Not Indexable") + .description(format!( + "The property {} cannot be accessed. This value does not support properties.", + name.error() + )) + .reference( + format!("The {} is referenced here.", "value".highlight()), + code_frame(Focus::Highlight, value_id), + ) + .suggestion("Remove the property access and use the value directly."), + + // TODO: cannot trigger this error + Self::PropertyNotFound(value_id, valid_properties, expected_property) => error + .title("Property Not Found") + .description(format!( + "There is no property named {} on this value.", + expected_property.error() + )) + .reference( + format!("The {} is referenced here.", "value".highlight()), + code_frame(Focus::Highlight, value_id), + ) + .suggestion(format!( + "Declare a new property or select an existing property instead. + + {} {}", + "properties:".subtle(), + format_list(valid_properties) + )) + .example(example::object_properties("first", expected_property)), + + Self::DuplicateProperty(name) => error + .title("Duplicate Property") + .description(format!( + "This expression includes multiple properties named {name}.", + name = name.error(), + )) + .suggestion(format!( + "Remove or rename any repeated {name} properties to avoid conflict.", + name = name.highlight() + )) + .example(example::object_properties( + format!("{}_0", name), + format!("{}_1", name), + )), + + Self::NotSpreadable(node_id) => error + .title("Not Spreadable") + .description(format!( + "{} cannot be spread because it is not object-like.", + "This expression".error() + )) + .code_frame(code_frame(Focus::Error, node_id)), + + Self::UntypedParameter(name) => error + .title("Untyped Parameter") + .description(format!( + "The parameter {} is missing a type annotation.", + name.error() + )) + .suggestion(format!( + "Add an annotation describing the type of the {} parameter.", + name.highlight() + )) + .example(example::parameter_types()), + + Self::DefaultValueRejected((typedef_id, typedef_type), (default_id, default_type)) => { + let mut suggestion = format!( + "Remove the default value or replace it with a value accepted by {}.", + typedef_type.to_string().highlight() + ); + + let example_value = match typedef_type { + types::Shape(types::Type::Nil) => Some("nil"), + types::Shape(types::Type::Boolean) => Some("true"), + types::Shape(types::Type::Integer) => Some("123"), + types::Shape(types::Type::Float) => Some("4.56"), + types::Shape(types::Type::String) => Some("\"text\""), + types::Shape(types::Type::Style) => Some("style {}"), + _ => None, + }; + + if let Some(example_value) = example_value { + suggestion.push_str(&format!( + "\n\n {} {}", + "example:".subtle(), + example_value.highlight() + )); + } + + error + .title("Default Value Rejected") + .description(format!( + "The type of this {} does not match the parameter's type. + + {} {} + {} {}", + "default value".error(), + "expected:".subtle(), + typedef_type.to_string().success(), + "found:".subtle(), + default_type.to_string().error() + )) + .code_frame(code_frame(Focus::Error, default_id)) + .reference( + format!("The {} is declared here.", "parameter's type".highlight()), + code_frame(Focus::Highlight, typedef_id), + ) + .suggestion(suggestion) + } + + Self::NotCallable(value_id) => error + .title("Not Callable") + .description(format!( + "This expression is {} and cannot be called.", + "not a function".error() + )) + .reference( + format!("The {} is referenced here.", "expression".highlight()), + code_frame(Focus::Highlight, value_id), + ) + .suggestion(format!( + "Remove the arguments and parentheses {} to use this value directly.", + "()".highlight() + )), + + Self::UnexpectedArgument(argument_id, parameter_count) => error + .title("Unexpected Argument") + .description(format!( + "This {} was not expected by the function call.", + "argument".error() + )) + .code_frame(code_frame(Focus::Error, argument_id)) + .reference( + format!( + "The {} being called here expects {} arguments.", + "function".highlight(), + parameter_count.to_string().highlight() + ), + code_frame(Focus::Highlight, id), + ) + .suggestion("Remove this argument from the function call."), + + Self::MissingArgument(parameter_id, parameter_type) => error + .title("Missing Argument") + .description(format!( + "This {} expects an argument of type {} that was not provided.", + "function call".error(), + parameter_type.to_string().success() + )) + .reference( + "The unfulfilled parameter type is declared here.", + code_frame(Focus::Highlight, parameter_id), + ) + .suggestion(format!( + "Add an argument of type {} to the function call.", + parameter_type.to_string().highlight() + )), + + Self::ArgumentRejected( + (parameter_id, parameter_type), + (argument_id, argument_type), + ) => error + .title("Argument Rejected") + .description(format!( + "The type of {} does not match the expected type. + + {} {} + {} {}", + "this argument".error(), + "expected:".subtle(), + parameter_type.to_string().success(), + "actual:".subtle(), + argument_type.to_string().error(), + )) + .code_frame(code_frame(Focus::Error, argument_id)) + .reference( + format!( + "The {} of the argument is declared here.", + "expected type".highlight() + ), + code_frame(Focus::Highlight, parameter_id), + ) + .suggestion(format!( + "Replace this argument with a value of type {}.", + parameter_type.to_string().highlight() + )), + + Self::NotRenderable(node_id) => error + .title("Not Renderable") + .description(format!( + "The {} of this view cannot be rendered.", + "return value".error() + )) + .code_frame(code_frame(Focus::Error, node_id)) + .reference( + format!("The {} is declared here.", "view".highlight()), + code_frame(Focus::Highlight, id), + ) + .suggestion(format!( + "Return a value that can be rendered. +This includes {} and primitive types: {}.", + "element".success(), + format_list(["nil", "boolean", "integer", "float", "string"]), + )), + + Self::InvalidComponent(name, actual_type) => { + error.title("Invalid Component").description(format!( + "The variable {} does not reference a valid component type. + + {} {} + {} {}", + name.error(), + "expected:".subtle(), + "view {}".success(), + "actual:".subtle(), + actual_type.to_string().error(), + )) + } + + Self::ComponentTypo(start_tag, end_tag) => error + .title("Component Typo") + .description(format!( + "The start {} and end {} tags of this component do not match.", + format!("<{start_tag}>").error(), + format!("").error(), + )) + .suggestion("Change the end tag to match the start tag.") + .example(example::open_component(start_tag)), + + // this is only possible when parsing type modules + Self::InvalidAttributes(_) => error + .title("Invalid Attributes") + .description("The provided attributes are not an object type."), + + Self::UnexpectedAttribute(attribute_id, name) => error + .title("Unexpected Attribute") + .description(format!( + "An attribute named {} was not expected by this component.", + name.error() + )) + .code_frame(code_frame(Focus::Error, attribute_id)) + .reference( + format!("The {} is rendered here.", "component".highlight()), + code_frame(Focus::Highlight, id), + ) + .suggestion(format!( + "Remove the attribute {} from the component.", + name.highlight() + )), + + Self::MissingAttribute(attribute_id, attribute_name, attribute_type) => error + .title("Missing Attribute") + .description(format!( + "This component expects an attribute {} of type {} that was not provided.", + attribute_name.success(), + attribute_type.to_string().success() + )) + .reference( + format!( + "The {} of the missing attribute is declare here.", + "expected type".highlight() + ), + code_frame(Focus::Highlight, attribute_id), + ) + .suggestion(format!( + "Add an attribute {} of type {}.", + attribute_name.highlight(), + attribute_type.to_string().highlight() + )) + .example(example::component_attributes("first", attribute_name)), + + Self::AttributeRejected( + (attribute_id, attribute_name, attribute_type), + (argument_id, argument_type), + ) => error + .title("Attribute Rejected") + .description(format!( + "The type of the attribute {} does not match the expected type. + + {} {} + {} {}", + attribute_name.error(), + "expected:".subtle(), + attribute_type.to_string().success(), + "actual:".subtle(), + argument_type.to_string().error(), + )) + .code_frame(code_frame(Focus::Error, argument_id)) + .reference( + format!( + "The {} of the attribute is declared here.", + "expected type".highlight() + ), + code_frame(Focus::Highlight, attribute_id), + ) + .suggestion(format!( + "Replace the attribute {} with a value of type {}.", + attribute_name.highlight(), + attribute_type.to_string().highlight() + )), + + Self::BinaryOperationNotSupported(op, (lhs_id, lhs_type), (rhs_id, rhs_type)) => { + let mut builder = + error + .title("Binary Operation Not Supported") + .description(format!( + "The operator {} cannot be applied to the arguments provided.", + op.to_string().highlight().bold() + )); + + if let Some(type_) = lhs_type { + builder = builder.reference( + format!( + "The left-hand side of this operation has type {}.", + type_.to_string().highlight() + ), + code_frame(Focus::Highlight, lhs_id), + ); + } + + if let Some(type_) = rhs_type { + builder = builder.reference( + format!( + "The right-hand side of this operation has type {}.", + type_.to_string().highlight() + ), + code_frame(Focus::Highlight, rhs_id), + ); + } + + builder.suggestion(format!( + "Make sure that both arguments have the correct types for this operator.\n{}", + match op { + ast::BinaryOperator::Add + | ast::BinaryOperator::Subtract + | ast::BinaryOperator::Multiply + | ast::BinaryOperator::Divide + | ast::BinaryOperator::Exponent + | ast::BinaryOperator::LessThan + | ast::BinaryOperator::LessThanOrEqual + | ast::BinaryOperator::GreaterThan + | ast::BinaryOperator::GreaterThanOrEqual => format!( + "The operator {} can only be applied to numbers like {} or {}.", + op.to_string().highlight().bold(), + "integer".success(), + "float".success(), + ), + + ast::BinaryOperator::And | ast::BinaryOperator::Or => format!( + "The operator {} can only be applied to {} values.", + op.to_string().highlight().bold(), + "boolean".success(), + ), + + ast::BinaryOperator::Equal | ast::BinaryOperator::NotEqual => format!( + "The operator {} can only be applied to values of the same type.", + op.to_string().highlight().bold(), + ), + } + )) + } + + Self::UnaryOperationNotSupported(op, (rhs_id, rhs_type)) => { + let mut builder = + error + .title("Unary Operation Not Supported") + .description(format!( + "The operator {} cannot be applied to the argument provided.", + op.to_string().highlight().bold() + )); + + if let Some(type_) = rhs_type { + builder = builder.reference( + format!( + "The right-hand side of this operation has type {}.", + type_.to_string().highlight() + ), + code_frame(Focus::Highlight, rhs_id), + ); + } + + builder.suggestion(format!( + "Replace the argument with one matching the expected type for this operator. +{}", + match op { + ast::UnaryOperator::Absolute | ast::UnaryOperator::Negate => format!( + "The operator {} can only be applied to numbers like {} or {}.", + op.to_string().highlight().bold(), + "integer".success(), + "float".success(), + ), + + ast::UnaryOperator::Not => format!( + "The operator {} can only be applied to {} values.", + op.to_string().highlight().bold(), + "boolean".success(), + ), + } + )) + } + + Self::UnexpectedKind(_, kind) => error.title("Unexpected Kind").description(format!( + "This expression should be a {} but instead found a {}.", + format!("{kind} expression").success(), + format!("{kind} expression", kind = kind.invert()).error() + )), + } + .build() + } +} diff --git a/engine/src/report/error/code.rs b/engine/src/report/error/code.rs new file mode 100644 index 00000000..14242c3a --- /dev/null +++ b/engine/src/report/error/code.rs @@ -0,0 +1,64 @@ +pub trait ToCode { + fn to_code(&self) -> ErrorCode; +} + +#[derive(Clone, Copy, Debug)] +pub struct ErrorCode(pub u16); + +impl ErrorCode { + // 0xx - internal errors + + pub const UNREGISTERED_MODULE: Self = Self(000); + + // 1xx - environment errors + + pub const INVALID_WRITE_TARGET: Self = Self(100); + pub const INVALID_WRITE_TARGET_NOT_FOUND: Self = Self(101); + pub const INVALID_WRITE_TARGET_PERMISSION_DENIED: Self = Self(102); + // [103..109] reserved space for additional writing errors + pub const INVALID_GLOB: Self = Self(110); + pub const ROOT_DIRECTORY_NOT_FOUND: Self = Self(111); + pub const SOURCE_DIRECTORY_NOT_FOUND: Self = Self(112); + pub const SOURCE_DIRECTORY_NOT_RELATIVE: Self = Self(113); + pub const ENTRYPOINT_NOT_FOUND: Self = Self(114); + pub const ENTRYPOINT_NOT_RELATIVE: Self = Self(115); + pub const CLEANUP_FAILED: Self = Self(116); + + // 2xx - parsing errors + + pub const INVALID_SYNTAX: Self = Self(200); + + // 3xx - linking errors + + pub const MODULE_NOT_FOUND: Self = Self(300); + pub const IMPORT_CYCLE: Self = Self(301); + + // 4xx - analysis errors + + pub const NOT_INFERRABLE: Self = Self(400); + pub const NOT_FOUND: Self = Self(401); + pub const VARIANT_NOT_FOUND: Self = Self(402); + pub const DECLARATION_NOT_FOUND: Self = Self(403); + pub const NOT_INDEXABLE: Self = Self(404); + pub const PROPERTY_NOT_FOUND: Self = Self(405); + pub const DUPLICATE_PROPERTY: Self = Self(406); + pub const NOT_SPREADABLE: Self = Self(407); + pub const UNTYPED_PARAMETER: Self = Self(408); + pub const DEFAULT_VALUE_REJECTED: Self = Self(409); + pub const NOT_CALLABLE: Self = Self(410); + pub const UNEXPECTED_ARGUMENT: Self = Self(411); + pub const MISSING_ARGUMENT: Self = Self(412); + pub const ARGUMENT_REJECTED: Self = Self(413); + pub const NOT_RENDERABLE: Self = Self(414); + pub const INVALID_COMPONENT: Self = Self(415); + pub const COMPONENT_TYPO: Self = Self(416); + pub const INVALID_ATTRIBUTES: Self = Self(417); + pub const UNEXPECTED_ATTRIBUTE: Self = Self(418); + pub const MISSING_ATTRIBUTE: Self = Self(419); + pub const ATTRIBUTE_REJECTED: Self = Self(420); + pub const BINARY_OPERATION_NOT_SUPPORTED: Self = Self(421); + pub const UNARY_OPERATION_NOT_SUPPORTED: Self = Self(422); + pub const UNEXPECTED_KIND: Self = Self(423); + + // 5xx - generation errors +} diff --git a/engine/src/report/error/configuration.rs b/engine/src/report/error/configuration.rs new file mode 100644 index 00000000..e844a546 --- /dev/null +++ b/engine/src/report/error/configuration.rs @@ -0,0 +1,83 @@ +use super::{code::ToCode, Display, ErrorCode, ErrorDisplay}; +use kore::{color::Highlight, format::SeparateEach, pretty::Pretty}; +use std::path::PathBuf; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ConfigurationError { + InvalidGlob(Vec), + RootDirectoryNotFound(PathBuf), + SourceDirectoryNotFound(PathBuf), + SourceDirectoryNotRelative(PathBuf), + EntrypointNotFound(PathBuf), + EntrypointNotRelative(PathBuf), +} + +impl ToCode for ConfigurationError { + fn to_code(&self) -> ErrorCode { + match self { + Self::InvalidGlob(..) => ErrorCode::INVALID_GLOB, + Self::RootDirectoryNotFound(..) => ErrorCode::ROOT_DIRECTORY_NOT_FOUND, + Self::SourceDirectoryNotFound(..) => ErrorCode::SOURCE_DIRECTORY_NOT_FOUND, + Self::SourceDirectoryNotRelative(..) => ErrorCode::SOURCE_DIRECTORY_NOT_RELATIVE, + Self::EntrypointNotFound(..) => ErrorCode::ENTRYPOINT_NOT_FOUND, + Self::EntrypointNotRelative(..) => ErrorCode::ENTRYPOINT_NOT_RELATIVE, + } + } +} + +impl<'a> Display<'a> for ConfigurationError { + type Context = (); + + fn display(&'a self, (): Self::Context) -> ErrorDisplay<'a> { + let bind = |title, description| ErrorDisplay::simple(self.to_code(), title, description); + + match self { + Self::InvalidGlob(glob) => bind( + "Invalid Glob", + format!( + "the provided glob failed to resolve with error(s):\n\n{errors}", + errors = SeparateEach( + "\n", + &glob + .iter() + .map(|x| format!("\u{2022} {x}")) + .collect::>() + ) + ), + ), + + Self::RootDirectoryNotFound(path) => bind( + "Root Directory Not Found", + format!("No folder was found at the path {}.", path.pretty()), + ), + + Self::SourceDirectoryNotFound(path) => bind( + "Source Directory Not Found", + format!("No folder was found at the path {}.", path.pretty()), + ), + + Self::SourceDirectoryNotRelative(path) => bind( + "Source Directory Not Relative", + format!( + "The path to the source directory should be relative to the {} but found {}.", + "root_dir".highlight(), + path.pretty().error() + ), + ), + + Self::EntrypointNotFound(path) => bind( + "Entrypoint Not Found", + format!("No module was found at the path {}.", path.pretty()), + ), + + Self::EntrypointNotRelative(path) => bind( + "Entrypoint Not Relative", + format!( + "The path to the entrypoint should be relative to the {} but found {}.", + "source_dir".highlight(), + path.pretty().error() + ), + ), + } + } +} diff --git a/engine/src/report/error/environment.rs b/engine/src/report/error/environment.rs new file mode 100644 index 00000000..10fe2836 --- /dev/null +++ b/engine/src/report/error/environment.rs @@ -0,0 +1,50 @@ +use super::{code::ToCode, Display, ErrorCode, ErrorDisplay}; +use kore::pretty::Pretty; +use std::{io, path::PathBuf}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum EnvironmentError { + InvalidWriteTarget(PathBuf, io::ErrorKind), + CleanupFailed(PathBuf, io::ErrorKind), +} + +impl ToCode for EnvironmentError { + fn to_code(&self) -> ErrorCode { + match self { + Self::InvalidWriteTarget(.., io::ErrorKind::NotFound) => { + ErrorCode::INVALID_WRITE_TARGET_NOT_FOUND + } + Self::InvalidWriteTarget(.., io::ErrorKind::PermissionDenied) => { + ErrorCode::INVALID_WRITE_TARGET_PERMISSION_DENIED + } + Self::InvalidWriteTarget(..) => ErrorCode::INVALID_WRITE_TARGET, + Self::CleanupFailed(..) => ErrorCode::CLEANUP_FAILED, + } + } +} + +impl<'a> Display<'a> for EnvironmentError { + type Context = (); + + fn display(&'a self, (): Self::Context) -> ErrorDisplay<'a> { + let bind = |title, description| ErrorDisplay::simple(self.to_code(), title, description); + + match self { + Self::InvalidWriteTarget(path, error) => bind( + "Invalid Write Target", + format!( + "Attempted write to {} failed with error {error}.", + path.pretty() + ), + ), + + Self::CleanupFailed(path, error) => bind( + "Cleanup Failed", + format!( + "Attempted to delete {} but failed with error {error}.", + path.pretty() + ), + ), + } + } +} diff --git a/engine/src/report/error/execution.rs b/engine/src/report/error/execution.rs new file mode 100644 index 00000000..c39e4d04 --- /dev/null +++ b/engine/src/report/error/execution.rs @@ -0,0 +1,93 @@ +use super::{code::ToCode, Display, ErrorCode, ErrorContext, ErrorDisplay}; +use crate::Link; +use kore::{color::Highlight, format::SeparateEach, pretty::Pretty}; +use lang::CanonicalId; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ExecutionError { + // internal errors + UnregisteredModule(Link), + + // parsing errors + InvalidSyntax(Link), + + // linking errors + ModuleNotFound(Link), + ImportCycle(Vec), + + // analysis errors + AnalysisError(CanonicalId, analyze::Error), + // generation errors +} + +impl ToCode for ExecutionError { + fn to_code(&self) -> ErrorCode { + match self { + // internal errors + Self::UnregisteredModule(..) => ErrorCode::UNREGISTERED_MODULE, + + // parsing errors + Self::InvalidSyntax(..) => ErrorCode::INVALID_SYNTAX, + + // linking errors + Self::ModuleNotFound(..) => ErrorCode::MODULE_NOT_FOUND, + Self::ImportCycle(..) => ErrorCode::IMPORT_CYCLE, + + // analysis errors + Self::AnalysisError(_, err) => err.to_code(), + } + } +} + +impl<'a> Display<'a> for ExecutionError { + type Context = &'a ErrorContext; + + fn display(&'a self, context: Self::Context) -> super::ErrorDisplay<'a> { + let simple = |title, description| ErrorDisplay::simple(self.to_code(), title, description); + + match self { + // internal errors + Self::UnregisteredModule(link) => simple( + "Unregistered Module", + format!( + "A referenced module ({}) was not found when linking. + +{}", + link.to_path().pretty(), + "This should not be possible and represents a fatal internal error.".error() + ), + ), + + // parsing errors + Self::InvalidSyntax(link) => simple( + "Invalid Syntax", + format!( + "The file {} does not contain valid Knot code.", + link.to_path().pretty() + ), + ), + + // linking errors + Self::ModuleNotFound(link) => simple( + "Module Not Found", + format!("Unable to find module {}.", link.to_path().pretty()), + ), + + Self::ImportCycle(links) => simple( + "Import Cycle", + format!( + "An import cycle was found between the following modules: + +{}", + SeparateEach( + format!(" {} ", "->".subtle()), + links.iter().map(|x| x.to_path().pretty()) + ) + ), + ), + + // analysis errors + Self::AnalysisError(id, err) => err.display((id, context)), + } + } +} diff --git a/engine/src/report/error/mod.rs b/engine/src/report/error/mod.rs new file mode 100644 index 00000000..1ce3ff84 --- /dev/null +++ b/engine/src/report/error/mod.rs @@ -0,0 +1,165 @@ +mod analysis; +mod code; +mod configuration; +mod environment; +mod execution; + +use super::CodeFrame; +use code::ErrorCode; +pub use configuration::ConfigurationError; +pub use environment::EnvironmentError; +pub use execution::ExecutionError; +use kore::{ + color::{Colorize, Highlight}, + format::{indented, Indented}, +}; +use std::{collections::HashMap, fmt::Write}; + +pub trait Display<'a> { + type Context; + + fn display(&'a self, context: Self::Context) -> ErrorDisplay<'a>; +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ErrorContext { + pub root_dir: String, + pub modules: HashMap, + pub nodes: HashMap, +} + +pub struct ErrorDisplay<'a> { + code: ErrorCode, + title: String, + description: String, + code_frame: Option>, + references: Vec<(String, CodeFrame<'a>)>, + suggestion: Option, + examples: Vec<(String, String)>, +} + +impl<'a> ErrorDisplay<'a> { + pub fn simple(code: ErrorCode, title: &'a str, description: String) -> Self { + Self { + code, + title: title.to_owned(), + description, + code_frame: None, + references: vec![], + suggestion: None, + examples: vec![], + } + } +} + +impl<'a> std::fmt::Display for ErrorDisplay<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{title} {code}\n\n{description}", + title = self.title.error().bold().underline(), + code = format!("(E#{})", self.code.0).subtle(), + description = Indented(&self.description) + )?; + + if let Some(code_frame) = &self.code_frame { + write!(indented(f), "\n\n{code_frame}")?; + } + + for (description, code_frame) in &self.references { + write!(indented(f), "\n\n{description}\n\n{code_frame}")?; + } + + if let Some(suggestion) = &self.suggestion { + write!( + indented(f), + "\n\n{}\n\n{suggestion}", + "How to Fix".success().bold().underline() + )?; + } + + for (title, summary) in &self.examples { + write!( + indented(f), + "\n\n{label} {title}\n\n{summary}", + label = "Example:".highlight(), + title = title.highlight().bold().underline() + )?; + } + + Ok(()) + } +} + +#[derive(Default)] +pub struct ErrorDisplayBuilder<'a> { + code: Option, + title: Option, + description: Option, + code_frame: Option>, + references: Vec<(String, CodeFrame<'a>)>, + suggestion: Option, + examples: Vec<(String, String)>, +} + +impl<'a> ErrorDisplayBuilder<'a> { + pub const fn code(mut self, code: ErrorCode) -> Self { + self.code = Some(code); + self + } + + pub fn title(mut self, title: T) -> Self + where + T: AsRef, + { + self.title = Some(title.as_ref().to_owned()); + self + } + + pub fn description(mut self, description: T) -> Self + where + T: AsRef, + { + self.description = Some(description.as_ref().to_owned()); + self + } + + pub const fn code_frame(mut self, code_frame: CodeFrame<'a>) -> Self { + self.code_frame = Some(code_frame); + self + } + + pub fn reference(mut self, description: T, code_frame: CodeFrame<'a>) -> Self + where + T: AsRef, + { + self.references + .push((description.as_ref().to_owned(), code_frame)); + self + } + + pub fn suggestion(mut self, suggestion: T) -> Self + where + T: AsRef, + { + self.suggestion = Some(suggestion.as_ref().to_owned()); + self + } + + pub fn example(mut self, example: (String, String)) -> Self { + self.examples.push(example); + self + } + + pub fn build(self) -> ErrorDisplay<'a> { + ErrorDisplay { + code: self.code.expect("missing error code"), + title: self.title.expect("missing error title"), + description: self.description.expect("missing error description"), + code_frame: self.code_frame, + references: self.references, + suggestion: self.suggestion, + examples: self.examples, + } + } +} diff --git a/engine/src/report/example.rs b/engine/src/report/example.rs new file mode 100644 index 00000000..2f367d4f --- /dev/null +++ b/engine/src/report/example.rs @@ -0,0 +1,165 @@ +use kore::{ + color::{Colorize, Highlight}, + format::Indented, + str, +}; + +pub fn local_variables(name: &str) -> (String, String) { + ( + str!("Local Variables"), + format!( + "In this example the {} keyword is used to declare a local variable named {}. + +{}", + "let".highlight(), + name.highlight(), + Indented(format!("{} {} = 123;", "let".highlight(), name.highlight()).dimmed()) + ), + ) +} + +pub fn constants(name: T) -> (String, String) +where + T: AsRef, +{ + ( + str!("Constants"), + format!( + "In this example the {} keyword is used to declare a constant named {}. + +{}", + "const".highlight(), + name.as_ref().highlight(), + Indented( + format!( + "{} {} = 123;", + "const".highlight(), + name.as_ref().highlight() + ) + .dimmed() + ) + ), + ) +} + +pub fn enumerated_type_variants(first: T1, second: T2) -> (String, String) +where + T1: AsRef, + T2: AsRef, +{ + ( + str!("Enumerated Type Variants"), + format!( + "In this example the enumerated type has variants named {} and {} respectively. + +{}", + first.as_ref().highlight(), + second.as_ref().highlight(), + Indented( + format!( + "enum Example = + {} + {} +;", + format!("| {}", first.as_ref()).highlight(), + format!("| {}", second.as_ref()).highlight(), + ) + .dimmed() + ) + ), + ) +} + +pub fn object_properties(first: T1, second: T2) -> (String, String) +where + T1: AsRef, + T2: AsRef, +{ + ( + str!("Properties"), + format!( + "In this example properties named {} and {} are declared. +All property names must be unique. + +{}", + first.as_ref().highlight(), + second.as_ref().highlight(), + Indented( + format!( + "type Example = {{ + {}: integer, + {}: integer, +}};", + first.as_ref().highlight(), + second.as_ref().highlight() + ) + .dimmed() + ) + ), + ) +} + +pub fn parameter_types() -> (String, String) { + ( + str!("Parameter Types"), + format!( + "In the example below both parameters are declared to have type {}. + +{}", + "integer".highlight(), + Indented( + format!( + "func add(left{}, right{}) -> left + right;", + ": integer".highlight(), + ": integer".highlight(), + ) + .dimmed() + ) + ), + ) +} + +pub fn open_component(name: &str) -> (String, String) { + ( + str!("Open Component"), + format!( + "In the example below a component {} is rendered with raw text as a child. + +{}", + name.highlight(), + Indented( + format!( + "{}Hello, World!{}", + format!("<{name}>").highlight(), + format!("").highlight(), + ) + .dimmed() + ) + ), + ) +} + +pub fn component_attributes(first: T1, second: T2) -> (String, String) +where + T1: AsRef, + T2: AsRef, +{ + ( + str!("Component Attributes"), + format!( + "In the example below a component is rendered with attributes {} and {}. + +{}", + first.as_ref().highlight(), + second.as_ref().highlight(), + Indented( + format!( + "", + first.as_ref().highlight(), + second.as_ref().highlight() + ) + .dimmed() + ) + ), + ) +} diff --git a/engine/src/report/into_errors.rs b/engine/src/report/into_errors.rs new file mode 100644 index 00000000..e1d6dc19 --- /dev/null +++ b/engine/src/report/into_errors.rs @@ -0,0 +1,30 @@ +use super::{ExecutionError, Failure}; +use std::iter::once; + +pub trait IntoErrors { + fn into_errors(self) -> Box>; +} + +impl IntoErrors for ExecutionError { + fn into_errors(self) -> Box> { + Box::new(once(self)) + } +} + +impl IntoErrors for Vec { + fn into_errors(self) -> Box> { + Box::new(self.into_iter()) + } +} + +impl IntoErrors for crate::Internal<()> { + fn into_errors(self) -> Box> { + match self { + Ok(()) => vec![], + Err(err) => match *err { + Failure::Execution(errors) => errors, + }, + } + .into_errors() + } +} diff --git a/engine/src/report/mod.rs b/engine/src/report/mod.rs index a536bbd8..163f684b 100644 --- a/engine/src/report/mod.rs +++ b/engine/src/report/mod.rs @@ -1,42 +1,116 @@ mod code_frame; mod error; +mod example; +mod into_errors; mod reporter; -pub use code_frame::CodeFrame; -pub use error::Error; +pub use code_frame::{CodeFrame, Focus}; +use error::Display; +pub use error::{ConfigurationError, EnvironmentError, ErrorContext, ExecutionError}; +pub use into_errors::IntoErrors; +use kore::color::{ColoredString, Colorize, Highlight}; pub use reporter::Reporter; -use std::iter::once; -pub trait Errors { - type Iter: Iterator; - - fn errors(self) -> Self::Iter; +pub trait Enrich { + fn enrich(&self, root_dir: String, failure: Failure) -> Report { + failure.no_context(root_dir) + } } -impl Errors for Error { - type Iter = std::iter::Once; +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Report { + Configuration(ConfigurationError), + Environment(EnvironmentError), + Execution { + errors: Vec, + context: ErrorContext, + }, +} - fn errors(self) -> Self::Iter { - once(self) +impl Report { + #[cfg(feature = "test")] + pub const fn exec_errors(&self) -> Option<&Vec> { + match self { + Self::Execution { errors, .. } => Some(errors), + Self::Configuration(_) => None, + Self::Environment(_) => None, + } } } -impl Errors for Vec { - type Iter = std::vec::IntoIter; +impl std::fmt::Display for Report { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + fn format_bumper(error_count: usize) -> ColoredString { + format!( + "finished with {} {}", + error_count.to_string().bold(), + if error_count > 1 { "errors" } else { "error" } + ) + .error() + } + + fn write_single(f: &mut std::fmt::Formatter, error: T) -> std::fmt::Result + where + T: std::fmt::Display, + { + let bumper = format_bumper(1); + + writeln!(f, "{}\n", bumper)?; + writeln!(f, "{index} {error}\n", index = format!("{})", 1).error())?; + writeln!(f, "{}\n", bumper) + } + + match self { + Self::Configuration(error) => write_single(f, error.display(())), + + Self::Environment(error) => write_single(f, error.display(())), + + Self::Execution { errors, context } => { + let errors = errors + .iter() + .filter(|error| { + !matches!( + error, + ExecutionError::AnalysisError(_, analyze::Error::NotInferrable(_)) + ) + }) + .collect::>(); + + let bumper = format_bumper(errors.len()); - fn errors(self) -> Self::Iter { - self.into_iter() + writeln!(f, "{}\n", bumper)?; + + for (index, error) in errors.iter().enumerate() { + writeln!( + f, + "{index} {error}\n", + index = format!("{})", index + 1).error(), + error = error.display(context) + )?; + } + + writeln!(f, "{}\n", bumper) + } + } } } -impl Errors for crate::Result<()> { - type Iter = std::vec::IntoIter; +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Failure { + Execution(Vec), +} - fn errors(self) -> Self::Iter { +impl Failure { + pub fn no_context(self, root_dir: String) -> Report { match self { - Ok(()) => vec![], - Err(errs) => errs, + Self::Execution(errors) => Report::Execution { + errors, + context: ErrorContext { + root_dir, + modules: Default::default(), + nodes: Default::default(), + }, + }, } - .errors() } } diff --git a/engine/src/report/reporter.rs b/engine/src/report/reporter.rs index b30b3fa5..836ca4c5 100644 --- a/engine/src/report/reporter.rs +++ b/engine/src/report/reporter.rs @@ -1,5 +1,4 @@ -use super::Errors; -use crate::{Error, Result}; +use super::{ExecutionError, Failure, IntoErrors}; use std::{ cell::{Ref, RefCell, RefMut}, rc::Rc, @@ -7,14 +6,14 @@ use std::{ struct ReporterState { fail_fast: bool, - errors: Vec, + errors: Vec, } impl ReporterState { - pub const fn new(fail_fast: bool) -> Self { + pub fn new(fail_fast: bool) -> Self { Self { fail_fast, - errors: vec![], + errors: Default::default(), } } } @@ -38,10 +37,6 @@ impl Reporter { (*self.state).borrow_mut() } - fn errors(&self) -> Vec { - self.state_mut().errors.clone() - } - fn should_fail_early(&self) -> bool { self.state().fail_fast && self.should_fail() } @@ -50,36 +45,52 @@ impl Reporter { !self.state().errors.is_empty() } - pub fn report(&mut self, x: T) + /// add errors to state + fn extend(&mut self, x: T) where - T: Errors, + T: IntoErrors, { - self.state_mut().errors.extend(x.errors()); + self.state_mut().errors.extend(x.into_errors()); + } + + fn to_failure(&self) -> Failure { + let state = (*self.state).borrow(); + + Failure::Execution(state.errors.clone()) } - pub fn raise(&mut self, x: I) -> Result<()> + /// report an error + /// returns an `Err` if configured to fail fast otherwise `Ok` + pub fn raise(&mut self, x: T) -> crate::Internal<()> where - I: Errors, + T: IntoErrors, { - self.report(x); - self.catch_early() - } + self.extend(x); - pub fn catch_early(&self) -> Result<()> { if self.should_fail_early() { - Err(self.errors()) + Err(Box::new(self.to_failure())) } else { Ok(()) } } - pub fn catch(&self) -> Result<()> { + /// returns an `Err` if any errors have been reported otherwise `Ok` + pub fn flush(&self) -> crate::Internal<()> { if self.should_fail() { - Err(self.errors()) + Err(Box::new(self.to_failure())) } else { Ok(()) } } + + /// report an error and return the report + pub fn fail(&mut self, x: T) -> Failure + where + T: IntoErrors, + { + self.extend(x); + self.to_failure() + } } impl Clone for Reporter { diff --git a/engine/src/resolve/file_cache.rs b/engine/src/resolve/file_cache.rs index c0c46a1a..80bbd6d1 100644 --- a/engine/src/resolve/file_cache.rs +++ b/engine/src/resolve/file_cache.rs @@ -5,6 +5,15 @@ pub struct FileCache<'a, T>(FileSystem<'a>, T) where T: Resolver; +impl<'a, T> FileCache<'a, T> +where + T: Resolver, +{ + pub const fn new(cache_dir: &'a Path, inner: T) -> Self { + Self(FileSystem(cache_dir), inner) + } +} + impl<'a, T> Resolver for FileCache<'a, T> where T: Resolver, diff --git a/engine/src/resource/lib_html.kd b/engine/src/resource/lib_html.kd index 1d26cf21..1e42d6d5 100644 --- a/engine/src/resource/lib_html.kd +++ b/engine/src/resource/lib_html.kd @@ -31,48 +31,48 @@ type GlobalAttributes = { translate?: boolean, }; -view address (GlobalAttributes); -view article (GlobalAttributes); -view aside (GlobalAttributes); -view footer (GlobalAttributes); -view header (GlobalAttributes); -view h1 (GlobalAttributes); -view h2 (GlobalAttributes); -view h3 (GlobalAttributes); -view h4 (GlobalAttributes); -view h5 (GlobalAttributes); -view h6 (GlobalAttributes); -view main (GlobalAttributes); -view nav (GlobalAttributes); -view section (GlobalAttributes); +view address GlobalAttributes; +view article GlobalAttributes; +view aside GlobalAttributes; +view footer GlobalAttributes; +view header GlobalAttributes; +view h1 GlobalAttributes; +view h2 GlobalAttributes; +view h3 GlobalAttributes; +view h4 GlobalAttributes; +view h5 GlobalAttributes; +view h6 GlobalAttributes; +view main GlobalAttributes; +view nav GlobalAttributes; +view section GlobalAttributes; -view blockquote ({ +view blockquote { ...GlobalAttributes, cite?: string, -}); -view dd (GlobalAttributes); -view div (GlobalAttributes); -view dl (GlobalAttributes); -view dt (GlobalAttributes); -view figcaption (GlobalAttributes); -view figure (GlobalAttributes); -view hr (GlobalAttributes); -view li ({ +}; +view dd GlobalAttributes; +view div GlobalAttributes; +view dl GlobalAttributes; +view dt GlobalAttributes; +view figcaption GlobalAttributes; +view figure GlobalAttributes; +view hr GlobalAttributes; +view li { ...GlobalAttributes, value?: integer -}); -view menu (GlobalAttributes); -view ol ({ +}; +view menu GlobalAttributes; +view ol { ...GlobalAttributes, reversed?: boolean, start?: integer, type?: string, -}); -view p (GlobalAttributes); -view pre (GlobalAttributes); -view ul (GlobalAttributes); +}; +view p GlobalAttributes; +view pre GlobalAttributes; +view ul GlobalAttributes; -view a ({ +view a { ...GlobalAttributes, download?: boolean, href?: string, @@ -82,49 +82,49 @@ view a ({ rel?: string, target?: string, type?: string, -}); -view abbr (GlobalAttributes); -view b (GlobalAttributes); -view bdi (GlobalAttributes); -view bdo ({ +}; +view abbr GlobalAttributes; +view b GlobalAttributes; +view bdi GlobalAttributes; +view bdo { ...GlobalAttributes, dir?: string, -}); -view br (GlobalAttributes); -view cite (GlobalAttributes); -view code (GlobalAttributes); -view data ({ +}; +view br GlobalAttributes; +view cite GlobalAttributes; +view code GlobalAttributes; +view data { ...GlobalAttributes, value?: string, -}); -view dfn (GlobalAttributes); -view em (GlobalAttributes); -view i (GlobalAttributes); -view kbd (GlobalAttributes); -view mark (GlobalAttributes); -view q ({ +}; +view dfn GlobalAttributes; +view em GlobalAttributes; +view i GlobalAttributes; +view kbd GlobalAttributes; +view mark GlobalAttributes; +view q { ...GlobalAttributes, cite?: string, -}); -view rp (GlobalAttributes); -view rt (GlobalAttributes); -view ruby (GlobalAttributes); -view s (GlobalAttributes); -view samp (GlobalAttributes); -view small (GlobalAttributes); -view span (GlobalAttributes); -view strong (GlobalAttributes); -view sub (GlobalAttributes); -view sup (GlobalAttributes); -view time ({ +}; +view rp GlobalAttributes; +view rt GlobalAttributes; +view ruby GlobalAttributes; +view s GlobalAttributes; +view samp GlobalAttributes; +view small GlobalAttributes; +view span GlobalAttributes; +view strong GlobalAttributes; +view sub GlobalAttributes; +view sup GlobalAttributes; +view time { ...GlobalAttributes, datetime?: string, -}); -view u (GlobalAttributes); -view var (GlobalAttributes); -view wbr (GlobalAttributes); +}; +view u GlobalAttributes; +view var GlobalAttributes; +view wbr GlobalAttributes; -view area ({ +view area { ...GlobalAttributes, alt?: string, coords?: string, @@ -135,8 +135,8 @@ view area ({ rel?: string, shape?: string, target?: string, -}); -view audio ({ +}; +view audio { ...GlobalAttributes, autoplay?: boolean, controls?: boolean, @@ -147,8 +147,8 @@ view audio ({ muted?: boolean, preload?: string, src?: string, -}); -view img ({ +}; +view img { ...GlobalAttributes, alt?: string, crossorigin?: string, @@ -164,20 +164,20 @@ view img ({ srcset?: string, width?: integer, usemap?: string, -}); -view map ({ +}; +view map { ...GlobalAttributes, name?: string, -}); -view track ({ +}; +view track { ...GlobalAttributes, default?: boolean, kind?: string, label?: string, src?: string, srclang?: string, -}); -view video ({ +}; +view video { ...GlobalAttributes, autoplay?: boolean, controls?: boolean, @@ -193,16 +193,16 @@ view video ({ preload?: string, src?: string, width?: integer, -}); +}; -view embed ({ +view embed { ...GlobalAttributes, height?: integer, src?: string, type?: string, width?: integer, -}); -view iframe ({ +}; +view iframe { ...GlobalAttributes, allow?: string, allowfullscreen?: boolean, @@ -214,8 +214,8 @@ view iframe ({ src?: string, srcdoc?: string, width?: integer, -}); -view object ({ +}; +view object { ...GlobalAttributes, data?: string, form?: string, @@ -223,14 +223,14 @@ view object ({ name?: string, type?: string, width?: integer, -}); -view picture (GlobalAttributes); -view portal ({ +}; +view picture GlobalAttributes; +view portal { ...GlobalAttributes, referrerpolicy?: string, src?: string, -}); -view source ({ +}; +view source { ...GlobalAttributes, type?: string, src?: string, @@ -239,55 +239,55 @@ view source ({ media?: string, height?: integer, width?: integer, -}); +}; -view canvas ({ +view canvas { ...GlobalAttributes, height?: integer, width?: integer, -}); +}; -view del ({ +view del { ...GlobalAttributes, cite?: string, datetime?: string, -}); -view ins ({ +}; +view ins { ...GlobalAttributes, cite?: string, datetime?: string, -}); +}; -view caption (GlobalAttributes); -view col ({ +view caption GlobalAttributes; +view col { ...GlobalAttributes, span?: integer, -}); -view colgroup ({ +}; +view colgroup { ...GlobalAttributes, span?: integer, -}); -view table (GlobalAttributes); -view tbody (GlobalAttributes); -view td ({ +}; +view table GlobalAttributes; +view tbody GlobalAttributes; +view td { ...GlobalAttributes, colspan?: integer, headers?: string, rowspan?: integer, -}); -view tfoot (GlobalAttributes); -view th ({ +}; +view tfoot GlobalAttributes; +view th { ...GlobalAttributes, abbr?: string, colspan?: integer, headers?: string, rowspan?: integer, scope?: string, -}); -view thead (GlobalAttributes); -view tr (GlobalAttributes); +}; +view thead GlobalAttributes; +view tr GlobalAttributes; -view button ({ +view button { ...GlobalAttributes, autofocus?: boolean, disabled?: boolean, @@ -302,15 +302,15 @@ view button ({ popovertargetaction?: string, type?: string, value?: string, -}); -view datalist (GlobalAttributes); -view fieldset ({ +}; +view datalist GlobalAttributes; +view fieldset { ...GlobalAttributes, disabled?: boolean, form?: string, name?: string, -}); -view form ({ +}; +view form { ...GlobalAttributes, autocapitalize?: string, autocomplete?: string, @@ -322,8 +322,8 @@ view form ({ method?: string, novalidate?: boolean, target?: string, -}); -view input ({ +}; +view input { ...GlobalAttributes, accept?: string, alt?: string, @@ -359,13 +359,13 @@ view input ({ type?: string, value?: string, width?: integer, -}); -view label ({ +}; +view label { ...GlobalAttributes, for?: string, -}); -view legend (GlobalAttributes); -view meter ({ +}; +view legend GlobalAttributes; +view meter { ...GlobalAttributes, value?: float, min?: float, @@ -374,31 +374,31 @@ view meter ({ high?: float, optimum?: float, form?: string, -}); -view optgroup ({ +}; +view optgroup { ...GlobalAttributes, disabled?: boolean, label?: string, -}); -view option ({ +}; +view option { ...GlobalAttributes, disabled?: boolean, label?: string, selected?: boolean, value?: string, -}); -view output ({ +}; +view output { ...GlobalAttributes, for?: string, form?: string, name?: string, -}); -view progress ({ +}; +view progress { ...GlobalAttributes, max?: float, value?: float, -}); -view select ({ +}; +view select { ...GlobalAttributes, autocomplete?: string, autofocus?: boolean, @@ -408,8 +408,8 @@ view select ({ name?: string, required?: boolean, size?: integer, -}); -view textarea ({ +}; +view textarea { ...GlobalAttributes, autocapitalize?: string, autocomplete?: string, @@ -427,15 +427,15 @@ view textarea ({ rows?: integer, spellcheck?: string, wrap?: string, -}); +}; -view details ({ +view details { ...GlobalAttributes, open?: boolean, name?: string, -}); -view dialog ({ +}; +view dialog { ...GlobalAttributes, open?: boolean, -}); -view summary (GlobalAttributes); +}; +view summary GlobalAttributes; diff --git a/engine/src/state.rs b/engine/src/state.rs deleted file mode 100644 index 922c4728..00000000 --- a/engine/src/state.rs +++ /dev/null @@ -1,264 +0,0 @@ -use crate::{link::ImportGraph, Link, Result}; -use bimap::BiMap; -use lang::{ast, CanonicalId, Identify, NamespaceId}; -use std::{ - collections::HashMap, - fmt::{Debug, Display, Pointer}, - path::{Path, PathBuf}, -}; - -#[derive(Debug)] -pub enum Ast { - Program(ast::meta::Program), - Typings(ast::meta::Typings), -} - -impl Ast -where - Meta: Clone, -{ - pub fn analyze( - &self, - context: &analyze::Context, - ) -> analyze::Result<(Ast, analyze::TypeMap)> { - match self { - Self::Program(program) => analyze::analyze(context, program.clone()) - .map(|(typed, types)| (Ast::Program(typed), types)), - - Self::Typings(typings) => analyze::analyze(context, typings.clone()) - .map(|(typed, types)| (Ast::Typings(typed), types)), - } - } -} - -impl Ast { - pub fn id(&self) -> &CanonicalId { - match self { - Self::Program(x) => x.0.id(), - Self::Typings(x) => x.0.id(), - } - } - - pub fn exports(&self) -> HashMap { - match self { - Self::Program(x) => x.exports(), - Self::Typings(x) => x.exports(), - } - } -} - -impl Display for Ast -where - Meta: Display, -{ - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - Self::Program(x) => x.fmt(f), - Self::Typings(x) => x.fmt(f), - } - } -} - -pub trait Modules<'a> { - type Meta: Debug + 'a; - type Iter: Iterator)>; - - fn modules(&'a self) -> Result; -} - -// pub trait InternalModules<'a, T> { -// fn internal_modules(&'a self) -> Vec)>; -// } - -#[derive(Debug)] -pub struct Module -where - T: Debug, -{ - pub id: NamespaceId, - pub text: String, - pub ast: Ast, -} - -impl Module -where - T: Debug, -{ - pub const fn new(id: NamespaceId, text: String, ast: Ast) -> Self { - Self { id, text, ast } - } -} - -pub struct FromEntry(pub Link); - -pub struct FromGlob<'a> { - pub dir: &'a Path, - pub glob: &'a str, -} - -impl<'a> FromGlob<'a> { - pub fn to_paths(&'a self) -> std::result::Result, Vec> { - let FromGlob { dir, glob } = self; - - match glob::glob(&[dir.to_string_lossy().to_string().as_str(), glob].join("/")) { - Ok(x) => { - let (paths, errors) = x.fold((vec![], vec![]), |(mut paths, mut errors), x| { - match x { - Ok(path) => match path.strip_prefix(dir) { - Ok(x) => paths.push(x.to_path_buf()), - Err(_) => errors.push(format!( - "failed to strip prefix '{}' from path '{}'", - dir.display(), - path.display() - )), - }, - Err(err) => { - errors.push(err.to_string()); - } - } - - (paths, errors) - }); - - if errors.is_empty() { - Ok(paths) - } else { - Err(errors) - } - } - - Err(err) => Err(vec![err.to_string()]), - } - } -} - -pub struct Parsed { - pub modules: HashMap>, - pub lookup: BiMap, - pub ambient: analyze::AmbientMap, -} - -impl Parsed { - pub fn to_import_graph(&self) -> ImportGraph { - self.internal_modules() - .fold(ImportGraph::new(), |mut graph, (_, x)| { - graph.add_node(x.id); - graph - }) - } - - pub fn internal_modules(&self) -> impl Iterator)> { - self.modules - .iter() - .filter_map(|(link, module)| link.is_internal().then_some((link, module))) - } -} - -impl<'a> Modules<'a> for Parsed { - type Meta = (); - type Iter = std::collections::hash_map::Iter<'a, Link, Module>; - - fn modules(&'a self) -> Result { - Ok(self.modules.iter()) - } -} - -impl<'a> Modules<'a> for Result { - type Meta = (); - type Iter = std::collections::hash_map::Iter<'a, Link, Module>; - - fn modules(&'a self) -> Result { - match self { - Ok(x) => Ok(x.modules.iter()), - Err(err) => Err(err.clone()), - } - } -} - -pub struct Linked { - modules: HashMap>, - pub lookup: BiMap, - pub ambient: analyze::AmbientMap, - pub graph: ImportGraph, -} - -impl Linked { - pub fn new(state: Parsed, graph: ImportGraph) -> Self { - Self { - graph, - lookup: state.lookup, - ambient: state.ambient, - modules: state.modules, - } - } - - pub fn get_module(&self, link: &Link) -> Option<&Module<()>> { - self.modules.get(link) - } -} - -impl<'a> Modules<'a> for Linked { - type Meta = (); - type Iter = std::collections::hash_map::Iter<'a, Link, Module>; - - fn modules(&'a self) -> Result { - Ok(self.modules.iter()) - } -} - -impl<'a> Modules<'a> for Result { - type Meta = (); - type Iter = std::collections::hash_map::Iter<'a, Link, Module>; - - fn modules(&'a self) -> Result { - match self { - Ok(x) => Ok(x.modules.iter()), - Err(err) => Err(err.clone()), - } - } -} - -pub struct Analyzed { - modules: HashMap>, - pub lookup: BiMap, - pub ambient: analyze::AmbientMap, - pub graph: ImportGraph, -} - -impl Analyzed { - pub fn new(state: Linked, modules: HashMap>) -> Self { - Self { - modules, - graph: state.graph, - lookup: state.lookup, - ambient: state.ambient, - } - } - - pub fn internal_modules(&self) -> impl Iterator)> { - self.modules - .iter() - .filter_map(|(link, module)| link.is_internal().then_some((link, module))) - } -} - -impl<'a> Modules<'a> for Analyzed { - type Meta = ast::typed::Meta; - type Iter = std::collections::hash_map::Iter<'a, Link, Module>; - - fn modules(&'a self) -> Result { - Ok(self.modules.iter()) - } -} - -impl<'a> Modules<'a> for Result { - type Meta = ast::typed::Meta; - type Iter = std::collections::hash_map::Iter<'a, Link, Module>; - - fn modules(&'a self) -> Result { - match self { - Ok(x) => Ok(x.modules.iter()), - Err(err) => Err(err.clone()), - } - } -} diff --git a/engine/src/state/analyzed.rs b/engine/src/state/analyzed.rs new file mode 100644 index 00000000..52a6dab3 --- /dev/null +++ b/engine/src/state/analyzed.rs @@ -0,0 +1,34 @@ +use super::{base::Base, Linked, Module}; +use crate::{link::ImportGraph, report, Link}; +use std::{ + collections::HashMap, + ops::{Deref, DerefMut}, +}; + +pub struct Analyzed(pub Base, pub ImportGraph); + +impl Analyzed { + pub fn new(state: Linked, modules: HashMap>) -> Self { + Self(state.0.with_modules(modules), state.1) + } +} + +impl Deref for Analyzed { + type Target = Base; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Analyzed { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl report::Enrich for Analyzed { + fn enrich(&self, root_dir: String, failure: report::Failure) -> report::Report { + self.0.enrich(root_dir, failure) + } +} diff --git a/engine/src/state/ast.rs b/engine/src/state/ast.rs new file mode 100644 index 00000000..4634667b --- /dev/null +++ b/engine/src/state/ast.rs @@ -0,0 +1,74 @@ +use crate::Link; +use lang::{CanonicalId, Identify}; +use std::{ + collections::HashMap, + fmt::{Debug, Display, Pointer}, +}; + +#[derive(Clone, Debug)] +pub enum Ast { + Program(lang::ast::meta::Program), + Typings(lang::ast::meta::Typings), +} + +impl Ast { + pub fn to_links(&self, link: &Link) -> Vec { + let path = link.to_path(); + + if let Self::Program(program) = self { + program + .imports() + .iter() + .map(|x| Link::from_import(&path, x.0.value())) + .collect::>() + } else { + vec![] + } + } +} + +impl Ast +where + Meta: Clone, +{ + pub fn analyze( + &self, + context: &analyze::Context, + ) -> analyze::Result<(Ast, analyze::TypeMap)> { + match self { + Self::Program(program) => analyze::analyze(context, program.clone()) + .map(|(typed, types)| (Ast::Program(typed), types)), + + Self::Typings(typings) => analyze::analyze(context, typings.clone()) + .map(|(typed, types)| (Ast::Typings(typed), types)), + } + } +} + +impl Ast { + pub fn id(&self) -> &CanonicalId { + match self { + Self::Program(x) => x.0.id(), + Self::Typings(x) => x.0.id(), + } + } + + pub fn exports(&self) -> HashMap { + match self { + Self::Program(x) => x.exports(), + Self::Typings(x) => x.exports(), + } + } +} + +impl Display for Ast +where + Meta: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Program(x) => x.fmt(f), + Self::Typings(x) => x.fmt(f), + } + } +} diff --git a/engine/src/state/base.rs b/engine/src/state/base.rs new file mode 100644 index 00000000..1fb79eda --- /dev/null +++ b/engine/src/state/base.rs @@ -0,0 +1,261 @@ +use super::{Ast, Module}; +use crate::{report, Library, Link}; +use bimap::BiMap; +use kore::Incrementor; +use lang::{ + walk::{CommonVisitor, ProgramVisitor, TypingsVisitor, Walk}, + CanonicalId, NamespaceId, NodeId, Range, +}; +use std::{collections::HashMap, marker::PhantomData}; + +pub type ModuleIterator<'a, T> = + Box)> + 'a>; + +#[derive(Clone, Default)] +pub struct Base { + modules: HashMap>, + lookup: BiMap, + ambient: analyze::AmbientMap, +} + +impl Base { + pub const fn ambient(&self) -> &analyze::AmbientMap { + &self.ambient + } + + pub fn has_by_link(&self, link: &Link) -> bool { + self.modules.contains_key(link) + } + + pub fn get_id_by_link(&self, link: &Link) -> Option<&NamespaceId> { + self.lookup.get_by_left(link) + } + + pub fn get_link_by_id(&self, id: &NamespaceId) -> Option<&Link> { + self.lookup.get_by_right(id) + } + + pub fn get_module_by_link(&self, link: &Link) -> Option<&Module> { + self.modules.get(link) + } + + pub fn get_link_and_module_by_id(&self, id: &NamespaceId) -> Option<(&Link, &Module)> { + let link = self.get_link_by_id(id)?; + + Some((link, self.modules.get(link)?)) + } + + pub fn modules(&self) -> ModuleIterator { + Box::new(self.modules.iter()) + } + + pub fn internal_modules(&self) -> ModuleIterator { + Box::new( + self.modules + .iter() + .filter_map(|(link, module)| link.is_internal().then_some((link, module))), + ) + } + + pub fn register_library(&mut self, library: Library, link: Link, module: Module) { + if let Some(scope) = library.to_ambient_scope() { + self.ambient.insert(scope, module.id); + } + + self.modules.insert(link, module); + } + + pub fn register_module(&mut self, link: Link, module: Module) { + self.lookup.insert(link.clone(), module.id); + self.modules.insert(link, module); + } + + pub fn with_modules(self, modules: HashMap>) -> Base { + Base { + modules, + lookup: self.lookup, + ambient: self.ambient, + } + } +} + +impl Base +where + T: Clone, +{ + pub fn enrich(&self, root_dir: String, failure: report::Failure) -> report::Report { + match failure { + report::Failure::Execution(errors) => report::Report::Execution { + errors, + + context: report::ErrorContext { + root_dir, + + modules: self + .modules + .iter() + .map(|(link, module)| (module.id, (link.clone(), module.text.clone()))) + .collect(), + + nodes: self + .modules + .values() + .flat_map(|module| { + let visitor = Visitor::new(module.id); + + match module.ast.clone() { + Ast::Program(x) => x.walk(visitor), + Ast::Typings(x) => x.walk(visitor), + } + .1 + .nodes + .into_iter() + }) + .collect(), + }, + }, + } + } +} + +struct Visitor { + _context: PhantomData, + namespace_id: NamespaceId, + node_id: Incrementor, + nodes: HashMap, +} + +impl Visitor { + pub fn new(namespace_id: NamespaceId) -> Self { + Self { + _context: PhantomData, + namespace_id, + node_id: Default::default(), + nodes: Default::default(), + } + } + + pub fn bind(mut self, range: Range) -> ((), Self) { + self.nodes.insert( + CanonicalId(self.namespace_id, NodeId(self.node_id.increment())), + range, + ); + ((), self) + } +} + +impl CommonVisitor for Visitor { + type Context = (Range, T); + type Binding = (); + type TypeExpression = (); + + fn binding(self, _: lang::ast::Binding, _: Range) -> (Self::Binding, Self) { + ((), self) + } + + fn type_expression( + self, + _: lang::ast::TypeExpression, + c: Self::Context, + ) -> (Self::TypeExpression, Self) { + self.bind(c.0) + } +} + +impl ProgramVisitor for Visitor { + type Expression = (); + type Statement = (); + type Attribute = (); + type Component = (); + type Parameter = (); + type Declaration = (); + type Import = (); + type Module = (); + + fn expression( + self, + _: lang::ast::Expression, + c: Self::Context, + ) -> (Self::Expression, Self) { + self.bind(c.0) + } + + fn statement( + self, + _: lang::ast::Statement, + c: Self::Context, + ) -> (Self::Statement, Self) { + self.bind(c.0) + } + + fn attribute( + self, + _: lang::ast::Attribute, + c: Self::Context, + ) -> (Self::Attribute, Self) { + self.bind(c.0) + } + + fn component( + self, + _: lang::ast::Component, + c: Self::Context, + ) -> (Self::Component, Self) { + self.bind(c.0) + } + + fn parameter( + self, + _: lang::ast::Parameter, + c: Self::Context, + ) -> (Self::Parameter, Self) { + self.bind(c.0) + } + + fn declaration( + self, + _: lang::ast::Declaration< + Self::Binding, + Self::Expression, + Self::TypeExpression, + Self::Parameter, + Self::Module, + >, + c: Self::Context, + ) -> (Self::Declaration, Self) { + self.bind(c.0) + } + + fn import(self, _: lang::ast::Import, c: Self::Context) -> (Self::Import, Self) { + self.bind(c.0) + } + + fn module( + self, + _: lang::ast::Module, + c: Self::Context, + ) -> (Self::Module, Self) { + self.bind(c.0) + } +} + +impl TypingsVisitor for Visitor { + type TypeDeclaration = (); + type TypeModule = (); + + fn type_declaration( + self, + _: lang::ast::TypeDeclaration, + c: Self::Context, + ) -> (Self::TypeDeclaration, Self) { + self.bind(c.0) + } + + fn type_module( + self, + _: lang::ast::TypeModule, + c: Self::Context, + ) -> (Self::TypeModule, Self) { + self.bind(c.0) + } +} diff --git a/engine/src/state/linked.rs b/engine/src/state/linked.rs new file mode 100644 index 00000000..6bc2a91d --- /dev/null +++ b/engine/src/state/linked.rs @@ -0,0 +1,37 @@ +use super::{base::Base, Parsed}; +use crate::{link::ImportGraph, report}; +use lang::NamespaceId; +use std::ops::{Deref, DerefMut}; + +#[derive(Clone)] +pub struct Linked(pub Base<()>, pub ImportGraph); + +impl Linked { + pub fn new(state: Parsed, graph: ImportGraph) -> Self { + Self(state.0, graph) + } + + pub fn iter_graph(&self) -> impl Iterator + '_ { + self.1.iter() + } +} + +impl Deref for Linked { + type Target = Base<()>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Linked { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl report::Enrich for Linked { + fn enrich(&self, root_dir: String, failure: report::Failure) -> report::Report { + self.0.enrich(root_dir, failure) + } +} diff --git a/engine/src/state/mod.rs b/engine/src/state/mod.rs new file mode 100644 index 00000000..9af53394 --- /dev/null +++ b/engine/src/state/mod.rs @@ -0,0 +1,98 @@ +mod analyzed; +mod ast; +mod base; +mod linked; +mod parsed; + +use crate::{ + report::{Enrich, Report}, + ConfigurationError, IntoResult, Link, Result, +}; +pub use analyzed::Analyzed; +pub use ast::Ast; +pub use base::Base; +use lang::NamespaceId; +pub use linked::Linked; +pub use parsed::Parsed; +use std::{fmt::Debug, path::Path}; + +#[derive(Clone, Debug)] +pub struct Module { + pub id: NamespaceId, + pub text: String, + pub ast: Ast, +} + +impl Module { + pub const fn new(id: NamespaceId, text: String, ast: Ast) -> Self { + Self { id, text, ast } + } +} + +#[derive(Clone)] +pub struct FromEntry(pub Link); + +impl IntoResult for FromEntry { + type Value = Self; + + fn into_result(self) -> Result { + Ok(self) + } +} + +impl Enrich for FromEntry {} + +#[derive(Clone)] +pub struct FromPaths(pub Vec); + +impl IntoResult for FromPaths { + type Value = Self; + + fn into_result(self) -> Result { + Ok(self) + } +} + +impl Enrich for FromPaths {} + +pub struct FromGlob<'a> { + pub dir: &'a Path, + pub glob: &'a str, +} + +impl<'a> FromGlob<'a> { + pub fn to_paths(&'a self) -> Result { + let FromGlob { dir, glob } = self; + + match glob::glob(&[dir.to_string_lossy().to_string().as_str(), glob].join("/")) { + Ok(x) => { + let (paths, errors) = x.fold((vec![], vec![]), |(mut paths, mut errors), x| { + match x { + Ok(path) => match path.strip_prefix(dir) { + Ok(x) => paths.push(x.to_path_buf()), + Err(_) => errors.push(format!( + "failed to strip prefix '{}' from path '{}'", + dir.display(), + path.display() + )), + }, + Err(err) => { + errors.push(err.to_string()); + } + } + + (paths, errors) + }); + + if errors.is_empty() { + Ok(FromPaths(paths.iter().map(Link::from).collect())) + } else { + Err(errors) + } + } + + Err(err) => Err(vec![err.to_string()]), + } + .map_err(|errs| Box::new(Report::Configuration(ConfigurationError::InvalidGlob(errs)))) + } +} diff --git a/engine/src/state/parsed.rs b/engine/src/state/parsed.rs new file mode 100644 index 00000000..c51eb4fa --- /dev/null +++ b/engine/src/state/parsed.rs @@ -0,0 +1,74 @@ +use super::{base::Base, Ast, Module}; +use crate::{link::ImportGraph, report, Context, ExecutionError, Link}; +use kore::Incrementor; +use lang::NamespaceId; +use std::{ + cell::RefCell, + ops::{Deref, DerefMut}, + rc::Rc, +}; + +#[derive(Clone, Default)] +pub struct Parsed(pub Base<()>, pub Rc>); + +impl Parsed { + pub fn incrementor(&self) -> Rc> { + Rc::clone(&self.1) + } + + pub fn to_import_graph(&self) -> ImportGraph { + self.internal_modules() + .fold(ImportGraph::new(), |mut graph, (_, x)| { + graph.add_node(x.id); + graph + }) + } + + pub fn link_modules(&self, context: &mut Context) -> crate::Internal { + self.internal_modules() + .try_fold(self.to_import_graph(), |mut acc, (link, module)| { + let links = module.ast.to_links(link); + + for x in &links { + if let Some(x) = self.get_id_by_link(x) { + acc.add_edge(&module.id, x).ok(); + } else { + context.raise(ExecutionError::UnregisteredModule(x.clone()))?; + } + } + + Ok(acc) + }) + } + + pub fn register_source(&mut self, link: Link, text: String, ast: Ast<()>) { + let incrementor = self.incrementor(); + let module = Module { + id: NamespaceId(incrementor.borrow_mut().deref_mut().increment()), + text, + ast, + }; + + self.0.register_module(link, module); + } +} + +impl Deref for Parsed { + type Target = Base<()>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Parsed { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl report::Enrich for Parsed { + fn enrich(&self, root_dir: String, failure: report::Failure) -> report::Report { + self.0.enrich(root_dir, failure) + } +} diff --git a/engine/src/validate.rs b/engine/src/validate.rs index 08cc03b8..1acb3396 100644 --- a/engine/src/validate.rs +++ b/engine/src/validate.rs @@ -1,39 +1,49 @@ -use crate::{link::ImportGraph, state, Error, Result}; +use crate::{link::ImportGraph, state, Context, ExecutionError}; use kore::invariant; -pub struct Validator<'a>(pub &'a state::Parsed); +type Result = Option>; -impl<'a> Validator<'a> { - pub fn assert_no_import_cycles(&self, graph: &ImportGraph) -> Result<()> { +pub struct Validator<'a, R>(pub &'a mut Context); + +impl<'a, R> Validator<'a, R> { + pub fn validate(self, state: &state::Parsed, graph: &ImportGraph) -> crate::Internal<()> { + let errors = vec![Self::assert_no_import_cycles(state, graph)] + .into_iter() + .flat_map(std::option::Option::unwrap_or_default) + .collect::>(); + + self.0.raise(errors) + } + + fn assert_no_import_cycles(state: &state::Parsed, graph: &ImportGraph) -> Result { if !graph.is_cyclic() { - return Ok(()); + return None; } let errors = graph .cycles() .into_iter() .map(|x| { - Error::ImportCycle( + ExecutionError::ImportCycle( x.to_vec() .iter() .map(|x| { - self.0 - .lookup - .get_by_right(x) + state + .get_link_by_id(x) .unwrap_or_else(|| { invariant!("lookup did not contain module with id {x}") }) .clone() }) - .collect::>(), + .collect(), ) }) .collect::>(); if errors.is_empty() { - Ok(()) + None } else { - Err(errors) + Some(errors) } } } diff --git a/engine/src/write.rs b/engine/src/write.rs index 947d8706..11837c48 100644 --- a/engine/src/write.rs +++ b/engine/src/write.rs @@ -1,4 +1,4 @@ -use crate::{Error, Result}; +use crate::{EnvironmentError, Report, Result}; use std::{ fmt::Display, fs::{self, File}, @@ -14,9 +14,14 @@ impl Writer where T: Display, { - pub fn overwrite(&self, dir: &Path) -> Result { + pub fn overwrite(self, dir: &Path) -> Result { if dir.exists() { - fs::remove_dir_all(dir).map_err(|_| vec![Error::CleanupFailed(dir.to_path_buf())])?; + fs::remove_dir_all(dir).map_err(|err| { + Report::Environment(EnvironmentError::CleanupFailed( + dir.to_path_buf(), + err.kind(), + )) + })?; } fs::create_dir_all(dir).ok(); @@ -24,33 +29,35 @@ where self.write(dir) } - pub fn write(&self, dir: &Path) -> Result { - let mut count = 0; + pub fn write(self, dir: &Path) -> Result { + self.0.and_then(|files| { + let mut count = 0; - match &self.0 { - Ok(xs) => { - for (path, generated) in xs { - let path = dir.join(path); + for (path, generated) in files { + let path = dir.join(path); - if let Some(parent) = path.parent() { - fs::create_dir_all(parent).ok(); - } + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).ok(); + } - let mut writer = - BufWriter::new(File::create(&path).map_err(|x| { - vec![Error::InvalidWriteTarget(path.clone(), x.kind())] - })?); + let file = match File::create(&path) { + Ok(x) => Ok(x), - write!(writer, "{generated}").ok(); - writer.flush().ok(); + Err(err) => Err(Report::Environment(EnvironmentError::InvalidWriteTarget( + path.clone(), + err.kind(), + ))), + }?; - count += 1; - } + let mut writer = BufWriter::new(file); - Ok(count) + write!(writer, "{generated}").ok(); + writer.flush().ok(); + + count += 1; } - Err(err) => Err(err.clone()), - } + Ok(count) + }) } } diff --git a/examples/invalid/invalid_syntax/src/main.kn b/examples/invalid/200_invalid_syntax/src/main.kn similarity index 100% rename from examples/invalid/invalid_syntax/src/main.kn rename to examples/invalid/200_invalid_syntax/src/main.kn diff --git a/examples/invalid/module_not_found/src/main.kn b/examples/invalid/300_module_not_found/src/main.kn similarity index 100% rename from examples/invalid/module_not_found/src/main.kn rename to examples/invalid/300_module_not_found/src/main.kn diff --git a/examples/invalid/import_cycle/src/first.kn b/examples/invalid/301_import_cycle/src/first.kn similarity index 100% rename from examples/invalid/import_cycle/src/first.kn rename to examples/invalid/301_import_cycle/src/first.kn diff --git a/examples/invalid/import_cycle/src/main.kn b/examples/invalid/301_import_cycle/src/main.kn similarity index 100% rename from examples/invalid/import_cycle/src/main.kn rename to examples/invalid/301_import_cycle/src/main.kn diff --git a/examples/invalid/import_cycle/src/second.kn b/examples/invalid/301_import_cycle/src/second.kn similarity index 100% rename from examples/invalid/import_cycle/src/second.kn rename to examples/invalid/301_import_cycle/src/second.kn diff --git a/examples/invalid/401_not_found/src/main.kn b/examples/invalid/401_not_found/src/main.kn new file mode 100644 index 00000000..1c6797a0 --- /dev/null +++ b/examples/invalid/401_not_found/src/main.kn @@ -0,0 +1 @@ +const FOO = BAR; \ No newline at end of file diff --git a/examples/invalid/402_variant_not_found/src/main.kn b/examples/invalid/402_variant_not_found/src/main.kn new file mode 100644 index 00000000..b2f289c4 --- /dev/null +++ b/examples/invalid/402_variant_not_found/src/main.kn @@ -0,0 +1,6 @@ +enum Foo = + | Fizz + | Buzz +; + +const BAR = Foo.Bar; \ No newline at end of file diff --git a/examples/invalid/403_declaration_not_found/src/main.kn b/examples/invalid/403_declaration_not_found/src/main.kn new file mode 100644 index 00000000..3835b4e4 --- /dev/null +++ b/examples/invalid/403_declaration_not_found/src/main.kn @@ -0,0 +1,3 @@ +module foo {} + +const BAR = foo.bar; \ No newline at end of file diff --git a/examples/invalid/404_not_indexable/src/main.kn b/examples/invalid/404_not_indexable/src/main.kn new file mode 100644 index 00000000..cb771744 --- /dev/null +++ b/examples/invalid/404_not_indexable/src/main.kn @@ -0,0 +1 @@ +const FOO = 123.foo; \ No newline at end of file diff --git a/examples/invalid/405_property_not_found/src/main.kn b/examples/invalid/405_property_not_found/src/main.kn new file mode 100644 index 00000000..5b2b128c --- /dev/null +++ b/examples/invalid/405_property_not_found/src/main.kn @@ -0,0 +1,4 @@ +type Foo = { + bar: string, +}; +type Fizz = Foo.fizz; \ No newline at end of file diff --git a/examples/invalid/406_duplicate_property/src/main.kn b/examples/invalid/406_duplicate_property/src/main.kn new file mode 100644 index 00000000..4809ca26 --- /dev/null +++ b/examples/invalid/406_duplicate_property/src/main.kn @@ -0,0 +1,4 @@ +type Foo = { + bar: string, + bar: string, +}; \ No newline at end of file diff --git a/examples/invalid/407_not_spreadable/src/main.kn b/examples/invalid/407_not_spreadable/src/main.kn new file mode 100644 index 00000000..3160e2f4 --- /dev/null +++ b/examples/invalid/407_not_spreadable/src/main.kn @@ -0,0 +1 @@ +type Foo = { ...integer }; \ No newline at end of file diff --git a/examples/invalid/408_untyped_parameter/src/main.kn b/examples/invalid/408_untyped_parameter/src/main.kn new file mode 100644 index 00000000..d005645c --- /dev/null +++ b/examples/invalid/408_untyped_parameter/src/main.kn @@ -0,0 +1 @@ +func foo(bar) -> {} \ No newline at end of file diff --git a/examples/invalid/409_default_value_rejected/src/main.kn b/examples/invalid/409_default_value_rejected/src/main.kn new file mode 100644 index 00000000..7fa259d3 --- /dev/null +++ b/examples/invalid/409_default_value_rejected/src/main.kn @@ -0,0 +1 @@ +func foo(bar: integer = true) -> {} \ No newline at end of file diff --git a/examples/invalid/410_not_callable/src/main.kn b/examples/invalid/410_not_callable/src/main.kn new file mode 100644 index 00000000..7c1f7cda --- /dev/null +++ b/examples/invalid/410_not_callable/src/main.kn @@ -0,0 +1 @@ +const FOO = 123(); \ No newline at end of file diff --git a/examples/invalid/411_unexpected_argument/src/main.kn b/examples/invalid/411_unexpected_argument/src/main.kn new file mode 100644 index 00000000..278873dd --- /dev/null +++ b/examples/invalid/411_unexpected_argument/src/main.kn @@ -0,0 +1,3 @@ +func foo -> nil; + +const FOO = foo(123); \ No newline at end of file diff --git a/examples/invalid/412_missing_argument/src/main.kn b/examples/invalid/412_missing_argument/src/main.kn new file mode 100644 index 00000000..f44ec63a --- /dev/null +++ b/examples/invalid/412_missing_argument/src/main.kn @@ -0,0 +1,3 @@ +func foo(bar: integer) -> nil; + +const FOO = foo(); \ No newline at end of file diff --git a/examples/invalid/413_argument_rejected/src/main.kn b/examples/invalid/413_argument_rejected/src/main.kn new file mode 100644 index 00000000..414fb165 --- /dev/null +++ b/examples/invalid/413_argument_rejected/src/main.kn @@ -0,0 +1,3 @@ +func foo(bar: integer) -> nil; + +const FOO = foo(true); \ No newline at end of file diff --git a/examples/invalid/414_not_renderable/src/main.kn b/examples/invalid/414_not_renderable/src/main.kn new file mode 100644 index 00000000..c94562a8 --- /dev/null +++ b/examples/invalid/414_not_renderable/src/main.kn @@ -0,0 +1 @@ +view Foo -> style {}; \ No newline at end of file diff --git a/examples/invalid/415_invalid_component/src/main.kn b/examples/invalid/415_invalid_component/src/main.kn new file mode 100644 index 00000000..82d81d4a --- /dev/null +++ b/examples/invalid/415_invalid_component/src/main.kn @@ -0,0 +1,3 @@ +const Foo = 123; + +const BAR = ; \ No newline at end of file diff --git a/examples/invalid/416_component_typo/src/main.kn b/examples/invalid/416_component_typo/src/main.kn new file mode 100644 index 00000000..171c15cd --- /dev/null +++ b/examples/invalid/416_component_typo/src/main.kn @@ -0,0 +1,3 @@ +view Foo -> nil; + +const FOO = ; \ No newline at end of file diff --git a/examples/invalid/418_unexpected_attribute/src/main.kn b/examples/invalid/418_unexpected_attribute/src/main.kn new file mode 100644 index 00000000..19bb1182 --- /dev/null +++ b/examples/invalid/418_unexpected_attribute/src/main.kn @@ -0,0 +1,3 @@ +view Foo -> nil; + +const FOO = ; \ No newline at end of file diff --git a/examples/invalid/419_missing_attribute/src/main.kn b/examples/invalid/419_missing_attribute/src/main.kn new file mode 100644 index 00000000..97594198 --- /dev/null +++ b/examples/invalid/419_missing_attribute/src/main.kn @@ -0,0 +1,3 @@ +view Foo(bar: integer) -> nil; + +const FOO = ; \ No newline at end of file diff --git a/examples/invalid/420_attribute_rejected/src/main.kn b/examples/invalid/420_attribute_rejected/src/main.kn new file mode 100644 index 00000000..cd56433a --- /dev/null +++ b/examples/invalid/420_attribute_rejected/src/main.kn @@ -0,0 +1,3 @@ +view Foo(bar: integer) -> nil; + +const FOO = ; \ No newline at end of file diff --git a/examples/invalid/421_binary_operation_not_supported/src/main.kn b/examples/invalid/421_binary_operation_not_supported/src/main.kn new file mode 100644 index 00000000..43200db0 --- /dev/null +++ b/examples/invalid/421_binary_operation_not_supported/src/main.kn @@ -0,0 +1 @@ +const FOO = 123 + true; \ No newline at end of file diff --git a/examples/invalid/422_unary_operation_not_supported/src/main.kn b/examples/invalid/422_unary_operation_not_supported/src/main.kn new file mode 100644 index 00000000..72260416 --- /dev/null +++ b/examples/invalid/422_unary_operation_not_supported/src/main.kn @@ -0,0 +1 @@ +const FOO = !"hello"; \ No newline at end of file diff --git a/examples/invalid/423_unexpected_kind/src/main.kn b/examples/invalid/423_unexpected_kind/src/main.kn new file mode 100644 index 00000000..db4a2051 --- /dev/null +++ b/examples/invalid/423_unexpected_kind/src/main.kn @@ -0,0 +1,5 @@ +const FOO = true; +type Bar = boolean; + +type Foo = FOO; +const BAR = Bar; \ No newline at end of file diff --git a/kore/src/color.rs b/kore/src/color.rs index 53077a2b..f2571839 100644 --- a/kore/src/color.rs +++ b/kore/src/color.rs @@ -44,3 +44,17 @@ pub trait Highlight: Colorize + Sized { } impl<'a> Highlight for &'a str {} + +pub trait ClearIf { + fn clear_if(self, condition: bool) -> Self; +} + +impl ClearIf for ColoredString { + fn clear_if(self, condition: bool) -> Self { + if condition { + self.clear() + } else { + self + } + } +} diff --git a/kore/src/format.rs b/kore/src/format.rs index a61ffb6c..e609d3fe 100644 --- a/kore/src/format.rs +++ b/kore/src/format.rs @@ -7,35 +7,22 @@ where indenter::indented(f).with_str(" ") } -struct Parameters<'a, T>(&'a Vec) -where - T: Display; - -impl<'a, T> Display for Parameters<'a, T> +pub struct SeparateEach(pub T, pub I) where T: Display, -{ - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - if self.0.is_empty() { - Ok(()) - } else { - write!(f, "({})", SeparateEach(", ", self.0)) - } - } -} + U: Display, + I: IntoIterator; -pub struct SeparateEach<'a, T>(pub &'a str, pub &'a Vec) -where - T: Display; - -impl<'a, T> Display for SeparateEach<'a, T> +impl Display for SeparateEach where T: Display, + U: Display, + I: Clone + IntoIterator, { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { let mut is_first = true; - for x in self.1 { + for x in self.1.clone() { if is_first { is_first = false; } else { @@ -48,16 +35,18 @@ where } } -pub struct PrefixEach<'a, T>(pub &'a str, pub &'a Vec) +pub struct PrefixEach<'a, T, I>(pub &'a str, pub I) where - T: Display; + T: Display, + I: IntoIterator; -impl<'a, T> Display for PrefixEach<'a, T> +impl<'a, T, I> Display for PrefixEach<'a, T, I> where T: Display, + I: Clone + IntoIterator, { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - for x in self.1 { + for x in self.1.clone() { write!(f, "{}{}", self.0, x)?; } @@ -65,16 +54,18 @@ where } } -pub struct SuffixEach<'a, T>(pub &'a str, pub &'a Vec) +pub struct SuffixEach<'a, T, I>(pub &'a str, pub I) where - T: Display; + T: Display, + I: IntoIterator; -impl<'a, T> Display for SuffixEach<'a, T> +impl<'a, T, I> Display for SuffixEach<'a, T, I> where T: Display, + I: Clone + IntoIterator, { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { - for x in self.1 { + for x in self.1.clone() { write!(f, "{}{}", x, self.0)?; } diff --git a/language/docs/ast.d2 b/language/docs/ast.d2 new file mode 100644 index 00000000..6610fd3b --- /dev/null +++ b/language/docs/ast.d2 @@ -0,0 +1,70 @@ +direction: up + +Binding +Program +Typings + +AST: { + Primitive + Expression + Statement + + Attribute + Component + + TypePrimitive + ObjectTypeExpressionEntry + TypeExpression + + Visibility + Storage + Parameter + Declaration + + ImportSource + Import + Module + + TypeDeclaration + TypeModule + + # relationships + + Expression -> Primitive + Expression -> Expression + Expression -> Statement + Expression -> Component + Statement -> Expression + + Attribute -> Expression + Component -> Attribute + Component -> Expression + + TypeExpression -> TypePrimitive + TypeExpression -> TypeExpression + TypeExpression -> ObjectTypeExpressionEntry + ObjectTypeExpressionEntry -> TypeExpression + + Storage -> Visibility + Parameter -> Expression + Parameter -> TypeExpression + Declaration -> Storage + Declaration -> Parameter + Declaration -> Expression + Declaration -> TypeExpression + Declaration -> Module + + Import -> ImportSource + Module -> Import + Module -> Declaration + + TypeDeclaration -> TypeExpression + TypeModule -> TypeDeclaration +} + +AST.Storage -> Binding +AST.ObjectTypeExpressionEntry -> Binding +AST.TypeDeclaration -> Binding + +Program -> AST.Module +Typings -> AST.TypeModule diff --git a/language/src/ast/meta.rs b/language/src/ast/meta.rs index e30f0c37..c4de4465 100644 --- a/language/src/ast/meta.rs +++ b/language/src/ast/meta.rs @@ -10,6 +10,12 @@ use std::fmt::Display; #[derive(Clone, Debug, PartialEq)] pub struct Binding(pub Node); +impl AsRef for Binding { + fn as_ref(&self) -> &str { + self.0.value().0.as_str() + } +} + impl Binding { pub const fn new(x: super::Binding, range: Range) -> Self { Self(Node::raw(x, range)) diff --git a/language/src/ast/operator.rs b/language/src/ast/operator.rs index 03b773b2..7e0b7d6b 100644 --- a/language/src/ast/operator.rs +++ b/language/src/ast/operator.rs @@ -1,6 +1,6 @@ use std::fmt::{Display, Formatter}; -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum UnaryOperator { /* logical */ Not, @@ -11,7 +11,7 @@ pub enum UnaryOperator { } impl Display for UnaryOperator { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match self { Self::Not => write!(f, "!"), @@ -21,7 +21,7 @@ impl Display for UnaryOperator { } } -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum BinaryOperator { /* logical */ And, @@ -46,7 +46,7 @@ pub enum BinaryOperator { } impl Display for BinaryOperator { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { match self { Self::And => write!(f, "&&"), Self::Or => write!(f, "||"), diff --git a/language/src/format/mod.rs b/language/src/format/mod.rs index 5ab0f5f6..fb843ea1 100644 --- a/language/src/format/mod.rs +++ b/language/src/format/mod.rs @@ -6,8 +6,8 @@ mod parameter; mod statement; mod types; -use kore::format::SeparateEach; -use std::fmt::{Display, Formatter}; +use kore::format::{indented, SeparateEach, SuffixEach}; +use std::fmt::{Display, Formatter, Result, Write}; struct Typedef<'a, TypeExpression>(&'a Option); @@ -15,7 +15,7 @@ impl<'a, TypeExpression> Display for Typedef<'a, TypeExpression> where TypeExpression: Display, { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter) -> Result { if let Some(typedef) = self.0 { write!(f, ": {}", typedef) } else { @@ -32,7 +32,7 @@ impl<'a, T> Display for Parameters<'a, T> where T: Display, { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter) -> Result { if self.0.is_empty() { Ok(()) } else { @@ -40,3 +40,42 @@ where } } } + +pub struct Lambda<'a, P, R>(pub &'a [P], pub R); + +impl<'a, P, R> Display for Lambda<'a, P, R> +where + P: Display, + R: Display, +{ + fn fmt(&self, f: &mut Formatter) -> Result { + write!( + f, + "({parameters}) -> {result}", + parameters = SeparateEach(", ", self.0), + result = self.1 + ) + } +} + +pub struct Object(pub I) +where + I: IntoIterator; + +impl Display for Object +where + T: Clone + Display, + I: Clone + IntoIterator, +{ + fn fmt(&self, f: &mut Formatter) -> Result { + let items = self.0.clone().into_iter().collect::>(); + + if items.is_empty() { + write!(f, "{{}}") + } else { + writeln!(f, "{{")?; + write!(indented(f), "{}", SuffixEach(",\n", items))?; + write!(f, "}}") + } + } +} diff --git a/language/src/format/types.rs b/language/src/format/types.rs index e07efff9..d5ef7657 100644 --- a/language/src/format/types.rs +++ b/language/src/format/types.rs @@ -1,6 +1,8 @@ -use crate::ast; -use kore::format::{indented, SeparateEach, SuffixEach}; -use std::fmt::{Display, Formatter, Write}; +use crate::{ast, format::Object}; +use kore::format::SuffixEach; +use std::fmt::{Display, Formatter}; + +use super::Lambda; impl Display for ast::TypePrimitive { fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { @@ -47,27 +49,9 @@ where Self::PropertyAccess(lhs, rhs) => write!(f, "{lhs}.{rhs}"), - Self::Function(parameters, result) => { - write!( - f, - "({parameters}) -> {result}", - parameters = SeparateEach(", ", parameters) - ) - } - - Self::Object(entries) if entries.is_empty() => { - write!(f, "{{}}") - } - - Self::Object(entries) => { - writeln!(f, "{{")?; - write!( - indented(f), - "{entries}", - entries = SuffixEach(",\n", entries) - )?; - write!(f, "}}") - } + Self::Function(parameters, result) => Lambda(parameters, result).fmt(f), + + Self::Object(entries) => Object(entries).fmt(f), } } } @@ -84,7 +68,7 @@ where Self::View { binding, attributes, - } => write!(f, "view {binding} ({attributes});",), + } => write!(f, "view {binding} {attributes};",), } } } @@ -308,10 +292,10 @@ mod tests { ])) )) .to_string(), - "view Foo ({ + "view Foo { bar: nil, fizz?: boolean, -});" +};" ); } @@ -334,7 +318,7 @@ mod tests { }) .to_string(), "type foo = nil; -view Bar ({}); +view Bar {}; " ); } diff --git a/language/src/fragment.rs b/language/src/fragment.rs index 070b0875..4e2d49fa 100644 --- a/language/src/fragment.rs +++ b/language/src/fragment.rs @@ -1,6 +1,8 @@ use crate::{ast, NodeId, ScopeId}; use std::collections::BTreeMap; +pub type FragmentMap = BTreeMap; + #[derive(Clone, Debug, PartialEq)] pub enum Fragment { /* program */ @@ -44,5 +46,3 @@ impl Fragment { } } } - -pub type FragmentMap = BTreeMap; diff --git a/language/src/lib.rs b/language/src/lib.rs index 43318b08..5917fe0b 100644 --- a/language/src/lib.rs +++ b/language/src/lib.rs @@ -12,13 +12,12 @@ mod type_of; pub mod types; pub mod walk; -use std::fmt::Display; - pub use fragment::{Fragment, FragmentMap}; pub use identify::Identify; pub use namespace::{Namespace, NamespaceKind}; pub use node::Node; pub use range::{Point, Range}; +use std::fmt::Display; pub use type_of::TypeOf; #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] diff --git a/language/src/namespace.rs b/language/src/namespace.rs index e46cddbe..4895402b 100644 --- a/language/src/namespace.rs +++ b/language/src/namespace.rs @@ -1,4 +1,5 @@ use crate::ast; +use kore::str; use std::path::{Path, PathBuf}; #[derive(Clone, Debug, Eq, Hash, PartialEq)] @@ -13,11 +14,11 @@ pub struct Namespace(pub NamespaceKind, pub Vec); impl Namespace { #[cfg(feature = "test")] - pub const MOCK: &Self = &Self(NamespaceKind::Internal, vec![]); + pub const MOCK: &'static Self = &Self(NamespaceKind::Internal, vec![]); #[cfg(feature = "test")] pub fn mock() -> Self { - Self(NamespaceKind::Internal, vec![]) + Self(NamespaceKind::Internal, vec![str!("mock")]) } pub fn from_path

(file_path: P, source: &ast::ImportSource, path: &[String]) -> Self @@ -64,7 +65,7 @@ impl Namespace { match kind { // TODO: this should never be implemented, maybe change to an invariant NamespaceKind::Library => unimplemented!("{self:?}"), - NamespaceKind::External(_namespace) => unimplemented!(), + NamespaceKind::External(_namespace) => unimplemented!("{self:?}"), NamespaceKind::Internal => (), } diff --git a/language/src/node.rs b/language/src/node.rs index 0db002fe..8d3d72ae 100644 --- a/language/src/node.rs +++ b/language/src/node.rs @@ -1,5 +1,5 @@ use crate::{walk::Walk, Range}; -use std::fmt::{Debug, Display, Formatter}; +use std::fmt::{Debug, Display}; #[derive(Clone, Debug, PartialEq)] pub struct Node(pub Value, pub Range, pub Meta); @@ -21,11 +21,17 @@ impl Node { &self.2 } - pub fn map_value(self, f: impl FnOnce(Value) -> T) -> Node { + pub fn map_value(self, f: F) -> Node + where + F: FnOnce(Value) -> T, + { Node(f(self.0), self.1, self.2) } - pub fn map_range(self, f: impl FnOnce(Range) -> Range) -> Self { + pub fn map_range(self, f: F) -> Self + where + F: FnOnce(Range) -> Range, + { Self(self.0, f(self.1), self.2) } @@ -60,7 +66,7 @@ impl Display for Node where Value: Display, { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { self.value().fmt(f) } } diff --git a/language/src/range.rs b/language/src/range.rs index ea6fc144..2703b365 100644 --- a/language/src/range.rs +++ b/language/src/range.rs @@ -1,4 +1,4 @@ -use std::ops::Add; +use std::{fmt::Display, ops::Add}; #[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, PartialOrd, Ord)] pub struct Point(pub usize, pub usize); @@ -13,6 +13,12 @@ impl Point { } } +impl Display for Point { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}:{}", self.0, self.1) + } +} + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct Range(pub Point, pub Point); diff --git a/language/src/types/mod.rs b/language/src/types/mod.rs index 144d3097..8d58a99a 100644 --- a/language/src/types/mod.rs +++ b/language/src/types/mod.rs @@ -1,9 +1,13 @@ mod shape; -pub use shape::ToShape; -use std::fmt::Debug; - -#[derive(Clone, Copy, Debug, PartialEq)] +use crate::{ + ast::TypePrimitive, + format::{Lambda, Object}, +}; +pub use shape::{ToShape, Type as Shape}; +use std::fmt::{Debug, Display, Pointer}; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Kind { Type, Value, @@ -11,12 +15,30 @@ pub enum Kind { } impl Kind { + pub const fn invert(&self) -> Self { + match self { + Self::Type => Self::Value, + Self::Value => Self::Type, + Self::Mixed => Self::Mixed, + } + } + pub fn can_accept(&self, other: &Self) -> bool { self == other || matches!((self, other), (Self::Mixed, _) | (_, Self::Mixed)) } } -#[derive(Clone, Debug, PartialEq)] +impl Display for Kind { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Type => write!(f, "type"), + Self::Value => write!(f, "value"), + Self::Mixed => write!(f, "mixed"), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] pub enum Enumerated { Declaration(Vec<(String, Vec)>), Variant(Vec, T), @@ -75,7 +97,7 @@ impl Enumerated { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum ObjectTypeEntry { Required(String, T), Optional(String, T), @@ -121,7 +143,20 @@ impl ObjectTypeEntry { } } -#[derive(Clone, Debug, PartialEq)] +impl Display for ObjectTypeEntry +where + T: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Required(binding, x) => write!(f, "{binding}: {x}"), + + Self::Optional(binding, x) => write!(f, "{binding}?: {x}"), + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] pub enum Type { Nil, Boolean, @@ -225,3 +260,38 @@ impl Type { self.map(&|_| ()) } } + +impl Display for Type +where + T: Display, +{ + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::Nil => Display::fmt(&TypePrimitive::Nil, f), + Self::Boolean => Display::fmt(&TypePrimitive::Boolean, f), + Self::Integer => Display::fmt(&TypePrimitive::Integer, f), + Self::Float => Display::fmt(&TypePrimitive::Float, f), + Self::String => Display::fmt(&TypePrimitive::String, f), + Self::Style => Display::fmt(&TypePrimitive::Style, f), + Self::Element => Display::fmt(&TypePrimitive::Element, f), + + Self::Enumerated(enumerated) => enumerated.fmt(f), + + Self::Function(parameters, result) => Lambda(parameters, result).fmt(f), + + Self::Object(entries) => Object(entries).fmt(f), + + Self::Module(entities) => write!( + f, + "module {}", + Object( + entities + .iter() + .map(|(name, _, type_)| format!("{name}: {type_}")) + ) + ), + + Self::View(attributes) => write!(f, "view ({})", Object(attributes)), + } + } +} diff --git a/language/src/types/shape.rs b/language/src/types/shape.rs index da2c2cbc..0c702af7 100644 --- a/language/src/types/shape.rs +++ b/language/src/types/shape.rs @@ -1,9 +1,18 @@ use crate::ast; -use std::rc::Rc; +use std::{ + fmt::{Debug, Display}, + rc::Rc, +}; -#[derive(Clone, Debug, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct Type(pub super::Type>); +impl Display for Type { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + Display::fmt(&self.0, f) + } +} + pub trait ToShape { fn to_shape(&self) -> Type; } diff --git a/parse/docs/architecture.d2 b/parse/docs/architecture.d2 new file mode 100644 index 00000000..fcf52bc9 --- /dev/null +++ b/parse/docs/architecture.d2 @@ -0,0 +1,9 @@ +parser: Parser +stream: Stream +result: Option +parse: Parse +parse.shape: oval + +parser -> parse +stream -> parse +parse -> result diff --git a/parse/src/lib.rs b/parse/src/lib.rs index 28c40f06..5f6046b6 100644 --- a/parse/src/lib.rs +++ b/parse/src/lib.rs @@ -14,7 +14,6 @@ use combine::{ easy::Errors, stream::position::{SourcePosition, Stream}, }; -// use lang::ast; pub type Result<'a, T> = std::result::Result< (T, Stream<&'a str, SourcePosition>), diff --git a/parse/src/types/type_declaration.rs b/parse/src/types/type_declaration.rs index 8af6e113..01c727ef 100644 --- a/parse/src/types/type_declaration.rs +++ b/parse/src/types/type_declaration.rs @@ -29,14 +29,10 @@ where m::terminated(( m::keyword("view"), m::binding(), - m::between( - m::symbol('('), - m::symbol(')'), - super::type_expression::type_expression(), - ), + super::type_expression::type_expression(), )) - .map(|((_, start), binding, (attributes, end))| { - let range = &start + &end; + .map(|((_, start), binding, attributes)| { + let range = &start + attributes.0.range(); ast::raw::TypeDeclaration::raw(ast::TypeDeclaration::view(binding, attributes), range) }) @@ -80,7 +76,7 @@ mod tests { #[test] fn view() { assert_eq!( - parse("view Foo ({ bar: nil, fizz?: boolean })").unwrap().0, + parse("view Foo { bar: nil, fizz?: boolean }").unwrap().0, ast::raw::TypeDeclaration::raw( ast::TypeDeclaration::view( ast::raw::Binding::new(ast::Binding(str!("Foo")), Range::new((1, 6), (1, 8))), @@ -89,28 +85,28 @@ mod tests { ast::ObjectTypeExpressionEntry::Required( ast::raw::Binding::new( ast::Binding(str!("bar")), - Range::new((1, 13), (1, 15)) + Range::new((1, 12), (1, 14)) ), ast::raw::TypeExpression::raw( ast::TypeExpression::Primitive(ast::TypePrimitive::Nil), - Range::new((1, 18), (1, 20)) + Range::new((1, 17), (1, 19)) ) ), ast::ObjectTypeExpressionEntry::Optional( ast::raw::Binding::new( ast::Binding(str!("fizz")), - Range::new((1, 23), (1, 26)) + Range::new((1, 22), (1, 25)) ), ast::raw::TypeExpression::raw( ast::TypeExpression::Primitive(ast::TypePrimitive::Boolean), - Range::new((1, 30), (1, 36)) + Range::new((1, 29), (1, 35)) ) ) ]), - Range::new((1, 11), (1, 38)) + Range::new((1, 10), (1, 37)) ) ), - Range::new((1, 1), (1, 39)) + Range::new((1, 1), (1, 37)) ) ); } diff --git a/parse/src/types/type_module.rs b/parse/src/types/type_module.rs index 3c63c2a7..a84a1a41 100644 --- a/parse/src/types/type_module.rs +++ b/parse/src/types/type_module.rs @@ -28,7 +28,7 @@ mod tests { assert_eq_sorted!( parse( "type foo = nil; -view Foo ({ bar: nil, fizz?: boolean });" +view Foo { bar: nil, fizz?: boolean };" ) .unwrap() .0, @@ -58,33 +58,33 @@ view Foo ({ bar: nil, fizz?: boolean });" ast::ObjectTypeExpressionEntry::Required( ast::raw::Binding::new( ast::Binding(str!("bar")), - Range::new((2, 13), (2, 15)) + Range::new((2, 12), (2, 14)) ), ast::raw::TypeExpression::raw( ast::TypeExpression::Primitive(ast::TypePrimitive::Nil), - Range::new((2, 18), (2, 20)) + Range::new((2, 17), (2, 19)) ) ), ast::ObjectTypeExpressionEntry::Optional( ast::raw::Binding::new( ast::Binding(str!("fizz")), - Range::new((2, 23), (2, 26)) + Range::new((2, 22), (2, 25)) ), ast::raw::TypeExpression::raw( ast::TypeExpression::Primitive( ast::TypePrimitive::Boolean ), - Range::new((2, 30), (2, 36)) + Range::new((2, 29), (2, 35)) ) ) ]), - Range::new((2, 11), (2, 38)) + Range::new((2, 10), (2, 37)) ) ), - Range::new((2, 1), (2, 39)) + Range::new((2, 1), (2, 37)) ) ]), - Range::new((1, 1), (2, 40)) + Range::new((1, 1), (2, 38)) ) ); } diff --git a/plugin_javascript/docs/javascript.d2 b/plugin_javascript/docs/javascript.d2 new file mode 100644 index 00000000..96fd4a08 --- /dev/null +++ b/plugin_javascript/docs/javascript.d2 @@ -0,0 +1,8 @@ +Expression +Statement +JavaScript + +Expression -> Expression +Expression -> Statement +Statement -> Expression +JavaScript -> Statement diff --git a/plugin_javascript/src/format/expression.rs b/plugin_javascript/src/format/expression.rs index 9eab0fdc..fd48c46e 100644 --- a/plugin_javascript/src/format/expression.rs +++ b/plugin_javascript/src/format/expression.rs @@ -54,7 +54,7 @@ impl Display for Expression { "{{{properties}}}", properties = Indented(Block(SuffixEach( ",\n", - &xs.iter().map(|(key, value)| Property(key, value)).collect() + xs.iter().map(|(key, value)| Property(key, value)) ))) ) } diff --git a/plugin_javascript/src/format/mod.rs b/plugin_javascript/src/format/mod.rs index cf1399e7..c6e3f466 100644 --- a/plugin_javascript/src/format/mod.rs +++ b/plugin_javascript/src/format/mod.rs @@ -3,10 +3,10 @@ mod statement; use crate::javascript::JavaScript; use kore::format::SuffixEach; -use std::fmt::{Display, Formatter}; +use std::fmt::Display; impl Display for JavaScript { - fn fmt(&self, f: &mut Formatter) -> std::fmt::Result { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { write!(f, "{statements}", statements = SuffixEach("\n", &self.0)) } } diff --git a/plugin_javascript/src/format/statement.rs b/plugin_javascript/src/format/statement.rs index 3ca50970..8a471b90 100644 --- a/plugin_javascript/src/format/statement.rs +++ b/plugin_javascript/src/format/statement.rs @@ -26,10 +26,7 @@ impl Display for Statement { "import {{ {imports} }} from \"{namespace}\";", imports = SeparateEach( ", ", - &imports - .iter() - .map(|(name, alias)| Import(name, alias)) - .collect() + imports.iter().map(|(name, alias)| Import(name, alias)) ) ) } diff --git a/plugin_javascript/src/transform/expression.rs b/plugin_javascript/src/transform/expression.rs index e4588ddb..d975fc21 100644 --- a/plugin_javascript/src/transform/expression.rs +++ b/plugin_javascript/src/transform/expression.rs @@ -179,7 +179,7 @@ impl Expression { } } - pub fn from_attributes(xs: &Vec, opts: &Options) -> Self { + pub fn from_attributes(xs: &[ast::shape::Attribute], opts: &Options) -> Self { if xs.is_empty() { return Self::Null; } diff --git a/plugin_javascript/src/transform/statement.rs b/plugin_javascript/src/transform/statement.rs index 4b86c855..94948887 100644 --- a/plugin_javascript/src/transform/statement.rs +++ b/plugin_javascript/src/transform/statement.rs @@ -42,10 +42,6 @@ impl Statement { value: &ast::shape::Declaration, opts: &Options, ) -> Vec { - fn parameter_name(suffix: &String) -> String { - format!("$param_{suffix}") - } - match &value.0 { ast::Declaration::TypeAlias { .. } => vec![], @@ -61,7 +57,7 @@ impl Statement { let parameters = variant_parameters .iter() .enumerate() - .map(|(index, _)| parameter_name(&index.to_string())) + .map(|(index, _)| format!("$param_{}", index)) .collect::>(); let results = [ @@ -218,13 +214,12 @@ impl Statement { .0 .declarations .iter() - .filter_map(|x| { - x.0.is_public().then(|| { - ( - x.0.binding().clone(), - Expression::Identifier(x.0.binding().clone()), - ) - }) + .filter(|x| x.0.is_public()) + .map(|x| { + ( + x.0.binding().clone(), + Expression::Identifier(x.0.binding().clone()), + ) }) .collect(), )))],