⭐️ A friendly language for building type-safe, scalable systems!
1

Configure Feed

Select the types of activity you want to include in your feed.

infer echo messages

authored by

Giacomo Cavalieri and committed by
Louis Pilfold
(Jul 6, 2025, 2:52 PM +0100) ea286e97 e1dfc296

+196 -74
+22 -3
compiler-core/src/ast/typed.rs
··· 144 144 location: SrcSpan, 145 145 type_: Arc<Type>, 146 146 expression: Option<Box<Self>>, 147 + message: Option<Box<Self>>, 147 148 }, 148 149 149 150 BitArray { ··· 242 243 } 243 244 } 244 245 245 - Self::Echo { expression, .. } => expression 246 + Self::Echo { 247 + expression, 248 + message, 249 + .. 250 + } => expression 246 251 .as_ref() 247 252 .and_then(|expression| expression.find_node(byte_index)) 253 + .or_else(|| { 254 + message 255 + .as_ref() 256 + .and_then(|message| message.find_node(byte_index)) 257 + }) 248 258 .or_else(|| self.self_if_contains_location(byte_index)), 249 259 250 260 Self::Todo { kind, .. } => match kind { ··· 507 517 tuple: expression, .. 508 518 } => expression.find_statement(byte_index), 509 519 510 - Self::Echo { expression, .. } => expression 520 + Self::Echo { 521 + expression, 522 + message, 523 + .. 524 + } => expression 511 525 .as_ref() 512 - .and_then(|expression| expression.find_statement(byte_index)), 526 + .and_then(|expression| expression.find_statement(byte_index)) 527 + .or_else(|| { 528 + message 529 + .as_ref() 530 + .and_then(|message| message.find_statement(byte_index)) 531 + }), 513 532 514 533 Self::BitArray { segments, .. } => segments 515 534 .iter()
+6
compiler-core/src/ast/untyped.rs
··· 111 111 112 112 Echo { 113 113 location: SrcSpan, 114 + /// This is the position where the echo keyword ends: 115 + /// ```gleam 116 + /// echo wibble 117 + /// // ^ ends here! 118 + /// ``` 119 + keyword_end: u32, 114 120 expression: Option<Box<Self>>, 115 121 message: Option<Box<Self>>, 116 122 },
+8 -2
compiler-core/src/ast/visit.rs
··· 92 92 location: &'ast SrcSpan, 93 93 type_: &'ast Arc<Type>, 94 94 expression: &'ast Option<Box<TypedExpr>>, 95 + message: &'ast Option<Box<TypedExpr>>, 95 96 ) { 96 - visit_typed_expr_echo(self, location, type_, expression); 97 + visit_typed_expr_echo(self, location, type_, expression, message); 97 98 } 98 99 99 100 fn visit_typed_expr_int( ··· 857 858 TypedExpr::Echo { 858 859 location, 859 860 expression, 861 + message, 860 862 type_, 861 - } => v.visit_typed_expr_echo(location, type_, expression), 863 + } => v.visit_typed_expr_echo(location, type_, expression, message), 862 864 } 863 865 } 864 866 ··· 1097 1099 _location: &'a SrcSpan, 1098 1100 _type_: &'a Arc<Type>, 1099 1101 expression: &'a Option<Box<TypedExpr>>, 1102 + message: &'a Option<Box<TypedExpr>>, 1100 1103 ) where 1101 1104 V: Visit<'a> + ?Sized, 1102 1105 { 1103 1106 if let Some(expression) = expression { 1104 1107 v.visit_typed_expr(expression) 1108 + } 1109 + if let Some(message) = message { 1110 + v.visit_typed_expr(message) 1105 1111 } 1106 1112 } 1107 1113
+6 -1
compiler-core/src/ast_folder.rs
··· 330 330 331 331 UntypedExpr::Echo { 332 332 location, 333 + keyword_end, 333 334 expression, 334 335 message, 335 - } => self.fold_echo(location, expression, message), 336 + } => self.fold_echo(location, keyword_end, expression, message), 336 337 337 338 UntypedExpr::Panic { location, message } => self.fold_panic(location, message), 338 339 ··· 382 383 UntypedExpr::Echo { 383 384 location, 384 385 expression, 386 + keyword_end, 385 387 message, 386 388 } => UntypedExpr::Echo { 387 389 location, 390 + keyword_end, 388 391 expression: expression.map(|expression| Box::new(self.fold_expr(*expression))), 389 392 message: message.map(|message| Box::new(self.fold_expr(*message))), 390 393 }, ··· 857 860 fn fold_echo( 858 861 &mut self, 859 862 location: SrcSpan, 863 + keyword_end: u32, 860 864 expression: Option<Box<UntypedExpr>>, 861 865 message: Option<Box<UntypedExpr>>, 862 866 ) -> UntypedExpr { 863 867 UntypedExpr::Echo { 864 868 location, 869 + keyword_end, 865 870 expression, 866 871 message, 867 872 }
+1
compiler-core/src/call_graph.rs
··· 197 197 UntypedExpr::Echo { 198 198 expression, 199 199 location: _, 200 + keyword_end: _, 200 201 message, 201 202 } => { 202 203 if let Some(expression) = expression {
+1
compiler-core/src/format.rs
··· 980 980 UntypedExpr::Echo { 981 981 expression, 982 982 location: _, 983 + keyword_end: _, 983 984 message, 984 985 } => self.echo(expression, message), 985 986
+2 -1
compiler-core/src/language_server/code_action.rs
··· 6705 6705 location: &'ast SrcSpan, 6706 6706 type_: &'ast Arc<Type>, 6707 6707 expression: &'ast Option<Box<TypedExpr>>, 6708 + message: &'ast Option<Box<TypedExpr>>, 6708 6709 ) { 6709 6710 // We also want to trigger the action if we're hovering over the expression 6710 6711 // being printed. So we create a unique span starting from the start of echo ··· 6745 6746 } 6746 6747 } 6747 6748 6748 - ast::visit::visit_typed_expr_echo(self, location, type_, expression); 6749 + ast::visit::visit_typed_expr_echo(self, location, type_, expression, message); 6749 6750 } 6750 6751 6751 6752 fn visit_typed_pipeline_assignment(&mut self, assignment: &'ast TypedPipelineAssignment) {
+5 -3
compiler-core/src/parse.rs
··· 547 547 } 548 548 } 549 549 550 - Some((start, Token::Echo, end)) => { 550 + Some((start, Token::Echo, echo_end)) => { 551 551 self.advance(); 552 552 if context == ExpressionUnitContext::FollowingPipe { 553 553 // If an echo is used as a step in a pipeline (`|> echo`) 554 554 // then it cannot be followed by an expression. 555 555 let message = self.maybe_parse_as_message()?; 556 - let end = message.as_ref().map_or(end, |m| m.location().end); 556 + let end = message.as_ref().map_or(echo_end, |m| m.location().end); 557 557 UntypedExpr::Echo { 558 558 location: SrcSpan { start, end }, 559 + keyword_end: echo_end, 559 560 expression: None, 560 561 message, 561 562 } ··· 567 568 // stop analysis from happening everywhere and be fault 568 569 // tolerant like everything else. 569 570 let expression = self.parse_expression()?; 570 - let end = expression.as_ref().map_or(end, |e| e.location().end); 571 + let end = expression.as_ref().map_or(echo_end, |e| e.location().end); 571 572 572 573 let message = self.maybe_parse_as_message()?; 573 574 let end = message.as_ref().map_or(end, |m| m.location().end); 574 575 575 576 UntypedExpr::Echo { 576 577 location: SrcSpan { start, end }, 578 + keyword_end: echo_end, 577 579 expression: expression.map(Box::new), 578 580 message, 579 581 }
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_at_start_of_pipeline_wraps_the_whole_thing.snap
··· 9 9 start: 0, 10 10 end: 26, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 PipeLine { 14 15 expressions: [
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_cannot_have_an_expression_in_a_pipeline.snap
··· 19 19 start: 6, 20 20 end: 10, 21 21 }, 22 + keyword_end: 10, 22 23 expression: None, 23 24 message: None, 24 25 },
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_followed_by_expression_ends_where_expression_ends.snap
··· 9 9 start: 0, 10 10 end: 11, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 Var { 14 15 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_has_lower_precedence_than_binop.snap
··· 9 9 start: 0, 10 10 end: 10, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 BinOp { 14 15 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_has_lower_precedence_than_pipeline.snap
··· 9 9 start: 0, 10 10 end: 28, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 PipeLine { 14 15 expressions: [
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_in_a_pipeline.snap
··· 19 19 start: 6, 20 20 end: 10, 21 21 }, 22 + keyword_end: 10, 22 23 expression: None, 23 24 message: None, 24 25 },
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_assert_and_message_1.snap
··· 32 32 start: 12, 33 33 end: 42, 34 34 }, 35 + keyword_end: 16, 35 36 expression: Some( 36 37 Int { 37 38 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_assert_and_message_2.snap
··· 14 14 start: 7, 15 15 end: 40, 16 16 }, 17 + keyword_end: 11, 17 18 expression: Some( 18 19 Var { 19 20 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_assert_and_messages_1.snap
··· 32 32 start: 12, 33 33 end: 42, 34 34 }, 35 + keyword_end: 16, 35 36 expression: Some( 36 37 Int { 37 38 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_assert_and_messages_2.snap
··· 14 14 start: 7, 15 15 end: 40, 16 16 }, 17 + keyword_end: 11, 17 18 expression: Some( 18 19 Var { 19 20 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_assert_and_messages_3.snap
··· 14 14 start: 7, 15 15 end: 42, 16 16 }, 17 + keyword_end: 11, 17 18 expression: Some( 18 19 BinOp { 19 20 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_block.snap
··· 9 9 start: 0, 10 10 end: 14, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 Block { 14 15 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_complex_expression.snap
··· 9 9 start: 0, 10 10 end: 34, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 Var { 14 15 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_let_assert_and_message.snap
··· 14 14 start: 15, 15 15 end: 45, 16 16 }, 17 + keyword_end: 19, 17 18 expression: Some( 18 19 Int { 19 20 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_let_assert_and_messages.snap
··· 14 14 start: 15, 15 15 end: 45, 16 16 }, 17 + keyword_end: 19, 17 18 expression: Some( 18 19 Int { 19 20 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_no_expressions_after_it.snap
··· 9 9 start: 0, 10 10 end: 4, 11 11 }, 12 + keyword_end: 4, 12 13 expression: None, 13 14 message: None, 14 15 },
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_no_expressions_after_it_but_a_message.snap
··· 9 9 start: 0, 10 10 end: 15, 11 11 }, 12 + keyword_end: 4, 12 13 expression: None, 13 14 message: Some( 14 15 Var {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_panic.snap
··· 9 9 start: 0, 10 10 end: 17, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 Panic { 14 15 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_panic_and_message.snap
··· 9 9 start: 0, 10 10 end: 17, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 Panic { 14 15 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_panic_and_messages.snap
··· 9 9 start: 0, 10 10 end: 24, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 Panic { 14 15 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_simple_expression_1.snap
··· 9 9 start: 0, 10 10 end: 22, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 Var { 14 15 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__echo_with_simple_expression_2.snap
··· 9 9 start: 0, 10 10 end: 24, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 Var { 14 15 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__panic_with_echo.snap
··· 15 15 start: 9, 16 16 end: 22, 17 17 }, 18 + keyword_end: 13, 18 19 expression: Some( 19 20 String { 20 21 location: SrcSpan {
+1
compiler-core/src/parse/snapshots/gleam_core__parse__tests__panic_with_echo_and_message.snap
··· 15 15 start: 9, 16 16 end: 31, 17 17 }, 18 + keyword_end: 13, 18 19 expression: Some( 19 20 Var { 20 21 location: SrcSpan {
+3
compiler-core/src/parse/snapshots/gleam_core__parse__tests__repeated_echos.snap
··· 9 9 start: 0, 10 10 end: 16, 11 11 }, 12 + keyword_end: 4, 12 13 expression: Some( 13 14 Echo { 14 15 location: SrcSpan { 15 16 start: 5, 16 17 end: 16, 17 18 }, 19 + keyword_end: 9, 18 20 expression: Some( 19 21 Echo { 20 22 location: SrcSpan { 21 23 start: 10, 22 24 end: 16, 23 25 }, 26 + keyword_end: 14, 24 27 expression: Some( 25 28 Int { 26 29 location: SrcSpan {
+34 -64
compiler-core/src/type_/expression.rs
··· 427 427 428 428 UntypedExpr::Echo { 429 429 location, 430 + keyword_end, 430 431 expression, 431 432 message, 432 - } => Ok(self.infer_echo(location, expression, message)), 433 + } => Ok(self.infer_echo(location, keyword_end, expression, message)), 433 434 434 435 UntypedExpr::Var { location, name, .. } => { 435 436 self.infer_var(name, location, ReferenceRegistration::RegisterReferences) ··· 581 582 582 583 self.purity = Purity::Impure; 583 584 584 - let message = message.map(|message| { 585 - // If there is a message expression then it must be a string. 586 - let message = self.infer(*message); 587 - if let Err(error) = unify(string(), message.type_()) { 588 - self.problems 589 - .error(convert_unify_error(error, message.location())); 590 - } 591 - Box::new(message) 592 - }); 593 - 585 + let message = message.map(|message| Box::new(self.infer_and_unify(*message, string()))); 594 586 TypedExpr::Todo { 595 587 location, 596 588 type_, ··· 615 607 PanicKind::Placeholder => {} 616 608 } 617 609 618 - let message = match message { 619 - None => None, 620 - Some(message) => { 621 - let message = self.infer(*message); 622 - if let Err(error) = unify(string(), message.type_()) { 623 - self.problems 624 - .error(convert_unify_error(error, message.location())) 625 - } 626 - Some(Box::new(message)) 627 - } 628 - }; 610 + let message = message.map(|message| Box::new(self.infer_and_unify(*message, string()))); 629 611 self.previous_panics = true; 630 612 TypedExpr::Panic { 631 613 location, ··· 637 619 fn infer_echo( 638 620 &mut self, 639 621 location: SrcSpan, 622 + keyword_end: u32, 640 623 expression: Option<Box<UntypedExpr>>, 641 624 message: Option<Box<UntypedExpr>>, 642 625 ) -> TypedExpr { 643 626 self.environment.echo_found = true; 644 627 self.purity = Purity::Impure; 645 628 646 - if let Some(expression) = expression { 629 + let expression = if let Some(expression) = expression { 647 630 let expression = self.infer(*expression); 648 631 if self.previous_panics { 649 632 self.warn_for_unreachable_code(location, PanicPosition::EchoExpression); 650 633 } 651 - TypedExpr::Echo { 652 - location, 653 - type_: expression.type_(), 654 - expression: Some(Box::new(expression)), 655 - } 634 + expression 656 635 } else { 636 + let location = SrcSpan { 637 + start: location.start, 638 + end: keyword_end, 639 + }; 657 640 self.problems 658 641 .error(Error::EchoWithNoFollowingExpression { location }); 659 642 self.error_expr(location) 643 + }; 644 + 645 + TypedExpr::Echo { 646 + location, 647 + type_: expression.type_(), 648 + expression: Some(Box::new(expression)), 649 + message: message.map(|message| Box::new(self.infer_and_unify(*message, string()))), 660 650 } 661 651 } 662 652 ··· 1589 1579 /// Same as `self.infer_or_error` but instead of returning a `Result` with an error, 1590 1580 /// records the error and returns an invalid expression. 1591 1581 /// 1592 - fn infer(&mut self, expression: UntypedExpr) -> TypedExpr { 1582 + pub fn infer(&mut self, expression: UntypedExpr) -> TypedExpr { 1593 1583 let location = expression.location(); 1594 1584 match self.infer_or_error(expression) { 1595 1585 Ok(result) => result, ··· 1598 1588 self.error_expr(location) 1599 1589 } 1600 1590 } 1591 + } 1592 + 1593 + /// Infers the type of the given function and tries to unify it with the 1594 + /// given type, recording any unification error that might take place. 1595 + /// The typed expression is returned in any case. 1596 + /// 1597 + pub fn infer_and_unify(&mut self, expression: UntypedExpr, type_: Arc<Type>) -> TypedExpr { 1598 + let expression = self.infer(expression); 1599 + if let Err(error) = unify(type_, expression.type_()) { 1600 + self.problems 1601 + .error(convert_unify_error(error, expression.location())) 1602 + } 1603 + expression 1601 1604 } 1602 1605 1603 1606 fn infer_binop( ··· 1949 1952 FeatureKind::LetAssertWithMessage, 1950 1953 message.location(), 1951 1954 ); 1952 - let message = self.infer_or_error(message).unwrap_or_else(|error| { 1953 - self.problems.error(error); 1954 - self.error_expr(location) 1955 - }); 1956 - 1957 - let _ = unify(string(), message.type_()).map_err(|e| { 1958 - self.problems 1959 - .error(convert_unify_error(e, message.location())) 1960 - }); 1961 - Some(message) 1955 + Some(self.infer_and_unify(message, string())) 1962 1956 } 1963 1957 None => None, 1964 1958 }; ··· 1996 1990 .error(convert_unify_error(error, value_location)), 1997 1991 } 1998 1992 1999 - let message = message.map(|message| { 2000 - let message_location = message.location(); 2001 - match self.infer_assert_message(message) { 2002 - Ok(value) => value, 2003 - Err(error) => { 2004 - self.problems.error(error); 2005 - self.error_expr(message_location) 2006 - } 2007 - } 2008 - }); 2009 - 1993 + let message = message.map(|message| self.infer_and_unify(message, string())); 2010 1994 self.track_feature_usage(FeatureKind::BoolAssert, location); 2011 1995 2012 1996 Assert { ··· 2014 1998 value, 2015 1999 message, 2016 2000 } 2017 - } 2018 - 2019 - fn infer_assert_message(&mut self, message: UntypedExpr) -> Result<TypedExpr, Error> { 2020 - let message_location = message.location(); 2021 - let message = self.infer(message); 2022 - 2023 - match unify(string(), message.type_()) { 2024 - Ok(()) => {} 2025 - Err(error) => self 2026 - .problems 2027 - .error(convert_unify_error(error, message_location)), 2028 - } 2029 - 2030 - Ok(message) 2031 2001 } 2032 2002 2033 2003 fn infer_case(
+4
compiler-core/src/type_/pipe.rs
··· 182 182 183 183 UntypedExpr::Echo { 184 184 location, 185 + keyword_end: _, 185 186 expression: None, 186 187 message, 187 188 } => { ··· 198 199 location, 199 200 expression: None, 200 201 type_: self.argument_type.clone(), 202 + message: message.map(|message| { 203 + Box::new(self.expr_typer.infer_and_unify(*message, string())) 204 + }), 201 205 }, 202 206 ) 203 207 }
+15
compiler-core/src/type_/tests/errors.rs
··· 2873 2873 } 2874 2874 2875 2875 #[test] 2876 + fn echo_followed_by_no_expression_and_message() { 2877 + assert_error!("echo as \"wibble\""); 2878 + } 2879 + 2880 + #[test] 2881 + fn echo_followed_by_no_expression_and_invalid_message() { 2882 + assert_error!("echo as 1"); 2883 + } 2884 + 2885 + #[test] 2886 + fn echo_followed_by_invalid_message() { 2887 + assert_error!("echo 11 as { True || False }"); 2888 + } 2889 + 2890 + #[test] 2876 2891 fn echo_followed_by_no_expression_2() { 2877 2892 assert_module_error!( 2878 2893 r#"
+21
compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__echo_followed_by_invalid_message.snap
··· 1 + --- 2 + source: compiler-core/src/type_/tests/errors.rs 3 + expression: "echo 11 as { True || False }" 4 + --- 5 + ----- SOURCE CODE 6 + echo 11 as { True || False } 7 + 8 + ----- ERROR 9 + error: Type mismatch 10 + ┌─ /src/one/two.gleam:1:12 11 + 12 + 1 │ echo 11 as { True || False } 13 + │ ^^^^^^^^^^^^^^^^^ 14 + 15 + Expected type: 16 + 17 + String 18 + 19 + Found type: 20 + 21 + Bool
+29
compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__echo_followed_by_no_expression_and_invalid_message.snap
··· 1 + --- 2 + source: compiler-core/src/type_/tests/errors.rs 3 + expression: echo as 1 4 + --- 5 + ----- SOURCE CODE 6 + echo as 1 7 + 8 + ----- ERROR 9 + error: Invalid echo use 10 + ┌─ /src/one/two.gleam:1:1 11 + 12 + 1 │ echo as 1 13 + │ ^^^^ I was expecting a value after this 14 + 15 + The `echo` keyword should be followed by a value to print. 16 + 17 + error: Type mismatch 18 + ┌─ /src/one/two.gleam:1:9 19 + 20 + 1 │ echo as 1 21 + │ ^ 22 + 23 + Expected type: 24 + 25 + String 26 + 27 + Found type: 28 + 29 + Int
+15
compiler-core/src/type_/tests/snapshots/gleam_core__type___tests__errors__echo_followed_by_no_expression_and_message.snap
··· 1 + --- 2 + source: compiler-core/src/type_/tests/errors.rs 3 + expression: "echo as \"wibble\"" 4 + --- 5 + ----- SOURCE CODE 6 + echo as "wibble" 7 + 8 + ----- ERROR 9 + error: Invalid echo use 10 + ┌─ /src/one/two.gleam:1:1 11 + 12 + 1 │ echo as "wibble" 13 + │ ^^^^ I was expecting a value after this 14 + 15 + The `echo` keyword should be followed by a value to print.