Skip to content

Implement bool assert #4468

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 37 commits into from
Apr 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6eb08d6
Implement parsing for bool `assert`
GearsDatapacks Mar 24, 2025
1a3ce7b
Add parsing for assert message
GearsDatapacks Mar 24, 2025
6cfd23f
Type check bool assert
GearsDatapacks Mar 24, 2025
ad7823f
Track feature usage of bool assert
GearsDatapacks Mar 24, 2025
f053cf8
Warn for asserting literal values
GearsDatapacks Mar 24, 2025
d8c8bd6
Format bool assert
GearsDatapacks Mar 24, 2025
27bbf5e
Implement code generation for bool `assert` on the javascript target
GearsDatapacks Mar 27, 2025
6e1ed1e
Add tests
GearsDatapacks Mar 27, 2025
2214b36
Properly wrap asserted expressions in brackets
GearsDatapacks Mar 27, 2025
aaffd75
Fix generation of asserted functions
GearsDatapacks Mar 28, 2025
a51195e
Add tests for assert with a message
GearsDatapacks Mar 28, 2025
083638b
Generate code for assert on erlang
GearsDatapacks Apr 1, 2025
96a7271
Use atom instead of strings on erlang
GearsDatapacks Apr 2, 2025
9be1b95
Properly generate code for short-circuiting operators when asserting …
GearsDatapacks Apr 2, 2025
ac677b8
Don't generate value field if expression is unevaluated
GearsDatapacks Apr 2, 2025
3b66d5c
Properly generate code for short-circuiting operators on js
GearsDatapacks Apr 4, 2025
6734856
Improve generation of assert `||` on erlang
GearsDatapacks Apr 4, 2025
0ac6f6a
Improve assert codegen code on erlang
GearsDatapacks Apr 5, 2025
e0861bb
Fix snapshots
GearsDatapacks Apr 5, 2025
1b436d0
Improve assert codegen code on js
GearsDatapacks Apr 5, 2025
c68fed6
Fix codegen of assert on erlang
GearsDatapacks Apr 5, 2025
f66357f
Add `assert` integration tests
GearsDatapacks Apr 6, 2025
97d67c5
Add test to CI
GearsDatapacks Apr 6, 2025
206b84c
Document, rename
GearsDatapacks Apr 6, 2025
5a08dfb
Clippy my beloved
GearsDatapacks Apr 6, 2025
5617fd2
Changelog
GearsDatapacks Apr 6, 2025
daff1f1
Update `gleam@@main` erlang template
GearsDatapacks Apr 6, 2025
8058e03
Document exception format
GearsDatapacks Apr 6, 2025
f4a18da
Add `assert_start`, `expression_start` and `expression_end` fields
GearsDatapacks Apr 6, 2025
623e3ba
Return `Nil` from bool `assert`
GearsDatapacks Apr 13, 2025
1e39032
Fix rebase
GearsDatapacks Apr 19, 2025
fb4644f
Address review comments
GearsDatapacks Apr 19, 2025
6612ae6
Clippy
GearsDatapacks Apr 19, 2025
e52554a
Improve literal detection
lpil Apr 22, 2025
d437329
Extra tests, for my understanding
lpil Apr 22, 2025
eeb8acf
Slightly change wording
lpil Apr 22, 2025
acecd44
Use `assert` in `gleam new`
lpil Apr 22, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -498,3 +498,7 @@ jobs:
- name: test/unicode_path
run: make
working-directory: ./test/unicode_path ⭐

- name: test/assert
run: make test-all
working-directory: ./test/assert
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,32 @@

([Giacomo Cavalieri](https://github.com/giacomocavalieri))

- You can now use the `assert` keyword by itself to test a boolean expression.
If the expression evaluates to `False` at runtime, the `assert` statement
will cause the program to panic, with information about the expression that
was asserted.

For example:

```gleam
pub fn ok_error_test() {
assert result.is_ok(Ok(10))
assert result.is_error(Error("Some error"))
assert Ok(1) != Error(1)
assert result.is_error(Ok(42)) // panic: Assertion failed
}
```

A custom panic message can also be provided in order to add extra information:

```gleam
pub fn identity_test() {
assert function.identity(True) as "Identity of True should never be False"
}
```

([Surya Rose](https://github.com/GearsDatapacks))

### Build tool

- The build tool now supports placing modules in a directory called `dev`,
Expand Down
7 changes: 4 additions & 3 deletions compiler-cli/src/new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,17 @@ pub fn main() -> Nil {{

Self::TestModule => Some(
r#"import gleeunit
import gleeunit/should

pub fn main() -> Nil {
gleeunit.main()
}

// gleeunit test functions end in `_test`
pub fn hello_world_test() {
1
|> should.equal(1)
let name = "Joe"
let greeting = "Hello, " <> name <> "!"

assert greeting == "Hello, Joe!"
}
"#
.into(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
---
source: compiler-cli/src/new/tests.rs
assertion_line: 56
expression: "crate::fs::read(Utf8PathBuf::from_path_buf(file_path.to_path_buf()).expect(\"Non Utf8 Path\"),).unwrap()"
snapshot_kind: text
---
import gleeunit
import gleeunit/should

pub fn main() -> Nil {
gleeunit.main()
}

// gleeunit test functions end in `_test`
pub fn hello_world_test() {
1
|> should.equal(1)
let name = "Joe"
let greeting = "Hello, " <> name <> "!"

assert greeting == "Hello, Joe!"
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
---
source: compiler-cli/src/new/tests.rs
assertion_line: 86
expression: "crate::fs::read(Utf8PathBuf::from_path_buf(file_path.to_path_buf()).expect(\"Non Utf8 Path\"),).unwrap()"
snapshot_kind: text
---
import gleeunit
import gleeunit/should

pub fn main() -> Nil {
gleeunit.main()
}

// gleeunit test functions end in `_test`
pub fn hello_world_test() {
1
|> should.equal(1)
let name = "Joe"
let greeting = "Hello, " <> name <> "!"

assert greeting == "Hello, Joe!"
}
56 changes: 53 additions & 3 deletions compiler-core/src/ast.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use crate::type_::expression::Implementations;
use crate::type_::printer::Names;
use crate::type_::{
self, Deprecation, ModuleValueConstructor, PatternConstructor, Type, TypedCallArg,
ValueConstructor,
ValueConstructor, nil,
};
use std::sync::Arc;

Expand Down Expand Up @@ -2673,6 +2673,8 @@ pub enum Statement<TypeT, ExpressionT> {
Assignment(Assignment<TypeT, ExpressionT>),
/// A `use` expression.
Use(Use<TypeT, ExpressionT>),
/// A bool assertion.
Assert(Assert<ExpressionT>),
}

pub type UntypedUse = Use<(), UntypedExpr>;
Expand Down Expand Up @@ -2782,6 +2784,7 @@ impl UntypedStatement {
Statement::Expression(expression) => expression.location(),
Statement::Assignment(assignment) => assignment.location,
Statement::Use(use_) => use_.location,
Statement::Assert(assert) => assert.location,
}
}

Expand All @@ -2790,13 +2793,14 @@ impl UntypedStatement {
Statement::Expression(expression) => expression.start_byte_index(),
Statement::Assignment(assignment) => assignment.location.start,
Statement::Use(use_) => use_.location.start,
Statement::Assert(assert) => assert.location.start,
}
}

pub fn is_placeholder(&self) -> bool {
match self {
Statement::Expression(expression) => expression.is_placeholder(),
Statement::Assignment(_) | Statement::Use(_) => false,
Statement::Assignment(_) | Statement::Use(_) | Statement::Assert(_) => false,
}
}
}
Expand All @@ -2807,6 +2811,7 @@ impl TypedStatement {
Statement::Expression(e) => e.is_println(),
Statement::Assignment(_) => false,
Statement::Use(_) => false,
Statement::Assert(_) => false,
}
}

Expand All @@ -2815,6 +2820,7 @@ impl TypedStatement {
Statement::Expression(expression) => expression.location(),
Statement::Assignment(assignment) => assignment.location,
Statement::Use(use_) => use_.location,
Statement::Assert(assert) => assert.location,
}
}

Expand All @@ -2826,14 +2832,16 @@ impl TypedStatement {
Statement::Expression(expression) => expression.last_location(),
Statement::Assignment(assignment) => assignment.value.last_location(),
Statement::Use(use_) => use_.call.last_location(),
Statement::Assert(assert) => assert.value.last_location(),
}
}

pub fn type_(&self) -> Arc<Type> {
match self {
Statement::Expression(expression) => expression.type_(),
Statement::Assignment(assignment) => assignment.type_(),
Statement::Use(_use) => _use.call.type_(),
Statement::Use(use_) => use_.call.type_(),
Statement::Assert(_) => nil(),
}
}

Expand All @@ -2842,6 +2850,7 @@ impl TypedStatement {
Statement::Expression(expression) => expression.definition_location(),
Statement::Assignment(_) => None,
Statement::Use(use_) => use_.call.definition_location(),
Statement::Assert(_) => None,
}
}

Expand All @@ -2856,6 +2865,13 @@ impl TypedStatement {
None
}
}),
Statement::Assert(assert) => assert.find_node(byte_index).or_else(|| {
if assert.location.contains(byte_index) {
Some(Located::Statement(self))
} else {
None
}
}),
}
}

Expand All @@ -2872,6 +2888,13 @@ impl TypedStatement {
}
})
}
Statement::Assert(assert) => assert.value.find_statement(byte_index).or_else(|| {
if assert.location.contains(byte_index) {
Some(self)
} else {
None
}
}),
}
}

Expand All @@ -2880,6 +2903,7 @@ impl TypedStatement {
Statement::Expression(expression) => expression.type_defining_location(),
Statement::Assignment(assignment) => assignment.location,
Statement::Use(use_) => use_.location,
Statement::Assert(assert) => assert.location,
}
}

Expand All @@ -2892,6 +2916,8 @@ impl TypedStatement {
!assignment.kind.is_assert() && assignment.value.is_pure_value_constructor()
}
Statement::Use(Use { call, .. }) => call.is_pure_value_constructor(),
// Assert statements by definition are not pure
Statement::Assert(_) => false,
}
}
}
Expand Down Expand Up @@ -2925,6 +2951,30 @@ impl TypedAssignment {
}
}

pub type TypedAssert = Assert<TypedExpr>;
pub type UntypedAssert = Assert<UntypedExpr>;

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Assert<Expression> {
pub location: SrcSpan,
pub value: Expression,
pub message: Option<Expression>,
}

impl TypedAssert {
pub fn find_node(&self, byte_index: u32) -> Option<Located<'_>> {
if let Some(found) = self.value.find_node(byte_index) {
return Some(found);
}
if let Some(message) = &self.message {
if let Some(found) = message.find_node(byte_index) {
return Some(found);
}
}
None
}
}

/// A pipeline is desugared to a series of assignments:
///
/// ```gleam
Expand Down
2 changes: 1 addition & 1 deletion compiler-core/src/ast/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ fn compile_module(src: &str) -> TypedModule {
fn get_bare_expression(statement: &TypedStatement) -> &TypedExpr {
match statement {
Statement::Expression(expression) => expression,
Statement::Use(_) | Statement::Assignment(_) => {
Statement::Use(_) | Statement::Assignment(_) | Statement::Assert(_) => {
panic!("Expected expression, got {statement:?}")
}
}
Expand Down
49 changes: 49 additions & 0 deletions compiler-core/src/ast/typed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -669,10 +669,43 @@ impl TypedExpr {
| Self::Tuple { .. }
| Self::String { .. }
| Self::BitArray { .. } => true,

// Calls are literals if they are records and all the arguemnts are also literals.
Self::Call { fun, args, .. } => {
fun.is_record_constructor()
&& args.iter().all(|argument| argument.value.is_literal())
}

// Variables are literals if they are record constructors that take no arguments.
Self::Var {
constructor:
ValueConstructor {
variant: ValueConstructorVariant::Record { arity: 0, .. },
..
},
..
} => true,

_ => false,
}
}

pub fn is_known_value(&self) -> bool {
match self {
Self::Int { .. }
| Self::List { .. }
| Self::Float { .. }
| Self::Tuple { .. }
| Self::String { .. }
| Self::BitArray { .. } => true,
TypedExpr::BinOp { left, right, .. } => left.is_known_value() && right.is_known_value(),
TypedExpr::NegateBool { value, .. } | TypedExpr::NegateInt { value, .. } => {
value.is_known_value()
}
expr => expr.is_record_builder(),
}
}

pub fn is_literal_string(&self) -> bool {
match self {
Self::String { .. } => true,
Expand Down Expand Up @@ -816,6 +849,21 @@ impl TypedExpr {
}
}

#[must_use]
pub fn is_record_constructor(&self) -> bool {
match self {
TypedExpr::Var {
constructor:
ValueConstructor {
variant: ValueConstructorVariant::Record { .. },
..
},
..
} => true,
_ => false,
}
}

#[must_use]
pub fn is_record_builder(&self) -> bool {
match self {
Expand All @@ -829,6 +877,7 @@ impl TypedExpr {
}
}

#[must_use]
/// If `self` is a record constructor, returns the nuber of arguments it
/// needs to be called. Otherwise, returns `None`.
///
Expand Down
17 changes: 16 additions & 1 deletion compiler-core/src/ast/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ use crate::type_::Type;

use super::{
AssignName, BinOp, BitArrayOption, CallArg, Definition, Pattern, PipelineAssignmentKind,
SrcSpan, Statement, TodoKind, TypeAst, TypedArg, TypedAssignment, TypedClause,
SrcSpan, Statement, TodoKind, TypeAst, TypedArg, TypedAssert, TypedAssignment, TypedClause,
TypedClauseGuard, TypedConstant, TypedCustomType, TypedDefinition, TypedExpr,
TypedExprBitArraySegment, TypedFunction, TypedModule, TypedModuleConstant, TypedPattern,
TypedPatternBitArraySegment, TypedPipelineAssignment, TypedStatement, TypedUse,
Expand Down Expand Up @@ -330,6 +330,10 @@ pub trait Visit<'ast> {
visit_typed_use(self, use_);
}

fn visit_typed_assert(&mut self, assert: &'ast TypedAssert) {
visit_typed_assert(self, assert);
}

fn visit_typed_pipeline_assignment(&mut self, assignment: &'ast TypedPipelineAssignment) {
visit_typed_pipeline_assignment(self, assignment);
}
Expand Down Expand Up @@ -1154,6 +1158,7 @@ where
Statement::Expression(expr) => v.visit_typed_expr(expr),
Statement::Assignment(assignment) => v.visit_typed_assignment(assignment),
Statement::Use(use_) => v.visit_typed_use(use_),
Statement::Assert(assert) => v.visit_typed_assert(assert),
}
}

Expand All @@ -1173,6 +1178,16 @@ where
// TODO: We should also visit the typed patterns!!
}

pub fn visit_typed_assert<'a, V>(v: &mut V, assert: &'a TypedAssert)
where
V: Visit<'a> + ?Sized,
{
v.visit_typed_expr(&assert.value);
if let Some(message) = &assert.message {
v.visit_typed_expr(message);
}
}

pub fn visit_typed_call_arg<'a, V>(v: &mut V, arg: &'a TypedCallArg)
where
V: Visit<'a> + ?Sized,
Expand Down
Loading
Loading