Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
77 changes: 37 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def factorial(x: Int) -> Int := match x with
0 => 1
n => do
def ans := 1
for i in 1 ..= n do ans := ans * i
for i in 1 ..= n do ans := ans * i end
ans
end
end
Expand All @@ -125,7 +125,7 @@ Lists make use of square brackets:
# lists
def a := [0, 2, 51]
def b := ["list", "of", "strings"]
def empty_list = []
def empty_list := []
# lists of tuples, builder syntax
def ab := [(x, y) | x in a, x > 0, y in b, b != "of" ]

Expand All @@ -141,13 +141,12 @@ def c := { 10, 20 }
def d := { 3 }
# sets, builder syntax
def cd := { x ^ y | x in c, y in d }
def empty_set := {,} # empty sets must have comma to distinguish from code block
def empty_set := {}

# maps
def e := { "do" => 1, "ree" => 2, "meee" => 3 }
# maps, builder syntax
def ef := { x => y - 2 | x in e, y = x.len() }
def empty_mapping := {=>}

# indexing works for lists and maps/mappings (sets cannot be indexed because these are unordered)
print(ab(2)) # prints '(2, "list")'
Expand Down Expand Up @@ -279,10 +278,6 @@ These are similar to interfaces in Java and Kotlin, and near identical to traits
In Mamba, we aim to have many small traits for a more idiomatic way to express the behaviour of objects/classes.
For those familiar with object-oriented programming, we favour a trait-based system over inheritance (like Rust, Mamba doesn't have inheritance).

> **Note** Generics (`trait Iterator[T]`), the `def <Trait> for <Class> where ...` external-implementation syntax,
> composing multiple parent traits, and `meta`/`fin` modifiers are not implemented yet;
> only a single optional parent trait via `trait X: Parent where ... end` works.

Consider example with iterators (which briefly showcases language generics):

```mamba
Expand All @@ -296,7 +291,7 @@ class RangeIter(_start: Int, _end: Int) where
end

def Iterator[Int] for RangeIter where
def has_next(self) -> Bool := self._current < self._stop
def has_next(self) -> Bool := self._current < self._end

def next(self) -> Int? := if self.has_next() then do
def value := self._current
Expand Down Expand Up @@ -359,6 +354,7 @@ def pure sin(x: Int) -> Int := do
def ans := x
for i in (1 ..= taylor).step(2) do
ans := ans + (x ^ (i + 2)) / (factorial (i + 2))
end
ans
end
```
Expand Down Expand Up @@ -424,7 +420,7 @@ However, this is ripe for abuse, so instead, we require that each argument imple
```mamba
# if we implement strictly decreasing, we must implement measure
# These are non-overridable method which uses this measure
trait def StrictlyDecreases: Measurable where
trait StrictlyDecreases: Measurable where
def fin meta decreases(self, other: Self) -> Bool := self.measure() < other.measure()
def fin meta equal(self, other: Self) -> Bool := self.measure() = other.measure()
def fin meta subtract(self, other: Self) -> Measurable := self.measure() - other.measure()
Expand Down Expand Up @@ -531,12 +527,12 @@ Instead, we now handle the error on-site:
```mamba
def m := Matrix(1.0, 2.0, 3.0, 4.0)

if m isa InvertibleMatrix then
if m.is_invertible() then
def inv := m.inverse()
else
print("Matrix is singular (not invertible).")

def last_op = m.last_op() ! where
def last_op := m.last_op() ! where
err: MatrixErr(message) => do
print("Error when getting last op: \"{message}\"")
"N/A" # optionally we can also return, but here we assign default value
Expand All @@ -550,23 +546,42 @@ In the above script, we will always print an error (gracefully) and assign some
Here we showcase how we try to handle errors on-site instead of in a (large) `try` block.
This also prevents us from wrapping large code blocks in a `try`, where it might not be clear what statement or expression might throw what error.

Under the hood, `<call> ! where <cases> end` desugars to a plain `match` on the call's result:

```mamba
match m.last_op() with
err: MatrixErr(message) => print("Error when getting last op: \"{message}\"")
end
```

This can also be combined with an assign.
In that case, we must either always return (halting execution or exiting the function), or evaluate to a value.
This is shown below:
This is shown below, assuming the following error classes and fallible function are defined:

```mamba
def a: Int := function_may_throw_err() ! where
err: MyErr => do
print("We have a problem: {err.message}.")
return # we return, halting execution
end
err: MyOtherErr => do
print("We have another problem: {err.message}.")
0 # ... or we assign default value 0 to a
class MyErr(message: Str): Exception(message)
class MyOtherErr(message: Str): Exception(message)

def function_may_throw_err() -> Int ! { MyErr, MyOtherErr } := 10
```

```mamba
def with_error_handling() := do
def a: Int := function_may_throw_err() ! where
err: MyErr => do
print("We have a problem: {err.message}.")
return # we return, halting execution
end
err: MyOtherErr => do
print("We have another problem: {err.message}.")
0 # ... or we assign default value 0 to a
end
end

print("a has value {a}.")
end

print("a has value {a}.")
with_error_handling()
```

We can also opt to not do any error handling, making the type of `a`:
Expand Down Expand Up @@ -603,24 +618,6 @@ a = a ! # Result[Int, MyErr] => Int, where if error case, an exception is raised
print("a has value {a}.")
```

Finally, we also introduce the `recover` keyword.
The intention is that instead of letting someone else up the stack perform cleanup, we can couple some of the cleanup at this site.
For instance, de-allocating resources which we no longer need.
This is similar to `drop` in Rust, though this applies only to errors/exceptions (as, generally speaking, we rely on garbage collection).
This is also similar to `finally` in Python, though we don't always run this block, only when we encounter an error.

The general syntax is `<expression-or-statement> recover <expression-or-statement>`
So:

```mamba
def a: Result[Int, MyErr] := function_may_throw_err() ! where
err: MyOtherErr => print("We have a problem: {err.message}.")
end recover do
print("cleaning up resource")
some_cleanup_function()
end
```

## 💽 Machine Output

There is an experimental feature where we output a very small subset of the language to machine code.
Expand Down
2 changes: 1 addition & 1 deletion docs/spec/grammar.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The grammar of the language in Extended Backus-Naur Form (EBNF).
| code-block

reassignment ::= expression ( ":=" | "+=" | "-=" | "*=" | "/=" | "^=" ) expression
call ::= expression [ ( "." | "?." ) ] id tuple [ "!" match-cases [ recover expression ] ]
call ::= expression [ ( "." | "?." ) ] id tuple [ "!" match-cases ]
raise ::= "!" id { "," id }

# for all collections, we require one comma at least to avoid ambiguity
Expand Down
1 change: 0 additions & 1 deletion docs/spec/reserved.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,6 @@ Keyword | Use
`then` | Denote start of then branch of if
`else` | Denote start of else branch of if
`match` | Denote start of a match expression or statement
`recover` | Recover from error, for (partial) local error recovery

## Control Flow Statements

Expand Down
26 changes: 26 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,32 @@ gets disallowed in future (detecting the cycle and rejecting it at check time),
should move to `tests/resource/invalid/type/class/` and get a `matches Err(_)` test_case instead
of being deleted outright, so the "was silently accepted" behavior isn't lost from history.

## Known checker gap: multi-variable builder/comprehension syntax

`[(x, y) | x in a, y in b]`-style builders (list, set, and dict alike) only resolve the *first*
bound variable when constructing the head expression's scope — a second `in` generator, or a
local `y = ...` binding, both produce "Undefined variable: y" even though the condition parses
and checks fine on its own. Confirmed with minimal repros for list-, set-, and dict-builders; only
a single bound variable (optionally filtered, e.g. `[x | x in a, x > 0]`) currently works. This
affects two of the `readme_example` fixtures (`lists`, `sets_maps` — see their `=> ignore[...]`
reasons in `tests/check/valid.rs`) and is called out in the README's Collections section.

## Known generator bug: `--annotate` can emit invalid Python for a shadowed loop variable in tail position

`readme_example/factorial_dynamic` (`tests/check/valid.rs`) is ignored because `--annotate`
(`Arguments { annotate: true, .. }`, which `tests_util::test_directory` always sets) generates
syntactically invalid Python when a `def`-shadowed loop variable is *also* the trailing expression
of a `match` case that becomes a function's return value. Root cause: `append_ret`
(`src/backend/python/convert/mod.rs`) blindly recurses into the *last statement* of a `Block` to
turn it into a `return`, but `wrap_scoped`'s (`src/backend/python/convert/control_flow.rs`)
scope-restore `if/else` (`if __mamba_i_existed: i = ... else: del i`) is appended as that last
statement for cleanup, not as the value — so `append_ret` recurses into the restore branches
themselves, emitting `return i = __mamba_i_saved` / `return del i`. `--annotate` is already
documented as "currently still buggy" in the CLI help (`src/cli.rs`); this is one concrete
reproduction of that. Fixing it means either having `wrap_scoped` run outside/after `append_ret`,
or teaching `append_ret` to skip over a trailing scope-restore `IfElse` and target the statement
before it instead.

## Flaky test: `tests/main.rs` under parallel execution

Seen once under `cargo llvm-cov` (which runs slower/instrumented) with default parallelism: one
Expand Down
46 changes: 20 additions & 26 deletions tests/check/valid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,32 +130,26 @@ use test_case::test_case;
#[test_case("operation", "boolean")]
#[test_case("operation", "equality_different_types")]
#[test_case("operation", "type_alias_primitive" => ignore["investigate whether this should in fact, pass"])]
#[test_case("reamde_example", "builtin_trait" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "class_with_constants" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "class" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "error_handling_as_expression" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "error_handling_desyntax_sugared" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "error_handling_early_exit" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "error_handling_handle_subset" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "error_handling_recover" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "error_handling" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "factorial_dynamic" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "factorial" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "impl_trait" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "list_shorthand" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "lists" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "mutability" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "pure_functions" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "sets_maps" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "total_functions" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "trait_fin_meta" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "trait_inheritance" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "traits" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "type_refinement_call_site" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "type_refinement_in_fun" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "type_refinement_matrix" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "type_refinement_on_matrix" => ignore["incorporate new AST in checker"])]
#[test_case("reamde_example", "type_refinement_set" => ignore["incorporate new AST in checker"])]
#[test_case("readme_example", "builtin_trait" => ignore["@ decorator syntax and meta trait/external impl for a built-in type not implemented"])]
#[test_case("readme_example", "class_with_constants")]
#[test_case("readme_example", "class")]
#[test_case("readme_example", "error_handling_as_expression")]
#[test_case("readme_example", "error_handling_desyntax_sugared" => ignore["match case cannot bind an error's constructor arguments yet (err: Type(args))"])]
#[test_case("readme_example", "error_handling_early_exit")]
#[test_case("readme_example", "error_handling_handle_subset" => ignore["explicit Result[...] type and re-raising via `<var> = <var> !` not implemented"])]
#[test_case("readme_example", "error_handling" => ignore["isa operator and match case capturing an error's constructor arguments not implemented"])]
#[test_case("readme_example", "factorial_dynamic" => ignore["--annotate generates invalid Python for a shadowed loop var that is also a match case's tail expression (documented as still buggy in the CLI help)"])]
#[test_case("readme_example", "factorial")]
#[test_case("readme_example", "impl_trait" => ignore["`def <Trait> for <Class> where ...` external-implementation syntax and meta modifier not implemented"])]
#[test_case("readme_example", "list_shorthand")]
#[test_case("readme_example", "lists" => ignore["list/set/dict builder syntax binding more than one variable is not resolved by the checker"])]
#[test_case("readme_example", "mutability")]
#[test_case("readme_example", "pure_functions" => ignore["range .step(...) method not implemented"])]
#[test_case("readme_example", "sets_maps" => ignore["list/set/dict builder syntax binding more than one variable is not resolved by the checker"])]
#[test_case("readme_example", "total_functions" => ignore["`total` keyword not implemented"])]
#[test_case("readme_example", "trait_fin_meta" => ignore["meta/fin modifiers on trait methods not implemented"])]
#[test_case("readme_example", "trait_inheritance" => ignore["composing multiple parent traits not implemented"])]
#[test_case("readme_example", "traits" => ignore["generics on traits and external-implementation syntax not implemented"])]
fn to_python(input_dir: &str, file_name: &str) -> OutTestRet {
tests_util::test_directory(true, &[input_dir], &[input_dir, "target"], file_name)
}
Expand Down
46 changes: 20 additions & 26 deletions tests/parse/valid.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,26 @@ use mamba::parse::result::ParseResult;
#[test_case("class", "types")]
#[test_case("class", "import")]
#[test_case("class", "trait_and_type")]
#[test_case("reamde_example", "builtin_trait" => ignore["rewrite parser"])]
#[test_case("reamde_example", "class_with_constants" => ignore["rewrite parser"])]
#[test_case("reamde_example", "class" => ignore["rewrite parser"])]
#[test_case("reamde_example", "error_handling_as_expression" => ignore["rewrite parser"])]
#[test_case("reamde_example", "error_handling_desyntax_sugared" => ignore["rewrite parser"])]
#[test_case("reamde_example", "error_handling_early_exit" => ignore["rewrite parser"])]
#[test_case("reamde_example", "error_handling_handle_subset" => ignore["rewrite parser"])]
#[test_case("reamde_example", "error_handling_recover" => ignore["rewrite parser"])]
#[test_case("reamde_example", "error_handling" => ignore["rewrite parser"])]
#[test_case("reamde_example", "factorial_dynamic" => ignore["rewrite parser"])]
#[test_case("reamde_example", "factorial" => ignore["rewrite parser"])]
#[test_case("reamde_example", "impl_trait" => ignore["rewrite parser"])]
#[test_case("reamde_example", "list_shorthand" => ignore["rewrite parser"])]
#[test_case("reamde_example", "lists" => ignore["rewrite parser"])]
#[test_case("reamde_example", "mutability" => ignore["rewrite parser"])]
#[test_case("reamde_example", "pure_functions" => ignore["rewrite parser"])]
#[test_case("reamde_example", "sets_maps" => ignore["rewrite parser"])]
#[test_case("reamde_example", "total_functions" => ignore["rewrite parser"])]
#[test_case("reamde_example", "trait_fin_meta" => ignore["rewrite parser"])]
#[test_case("reamde_example", "trait_inheritance" => ignore["rewrite parser"])]
#[test_case("reamde_example", "traits" => ignore["rewrite parser"])]
#[test_case("reamde_example", "type_refinement_call_site" => ignore["rewrite parser"])]
#[test_case("reamde_example", "type_refinement_in_fun" => ignore["rewrite parser"])]
#[test_case("reamde_example", "type_refinement_matrix" => ignore["rewrite parser"])]
#[test_case("reamde_example", "type_refinement_on_matrix" => ignore["rewrite parser"])]
#[test_case("reamde_example", "type_refinement_set" => ignore["rewrite parser"])]
#[test_case("readme_example", "builtin_trait" => ignore["@ decorator syntax and meta keyword not implemented"])]
#[test_case("readme_example", "class_with_constants")]
#[test_case("readme_example", "class")]
#[test_case("readme_example", "error_handling_as_expression")]
#[test_case("readme_example", "error_handling_desyntax_sugared" => ignore["match case cannot bind an error's constructor arguments yet (err: Type(args))"])]
#[test_case("readme_example", "error_handling_early_exit")]
#[test_case("readme_example", "error_handling_handle_subset")]
#[test_case("readme_example", "error_handling" => ignore["isa operator and match case capturing an error's constructor arguments not implemented"])]
#[test_case("readme_example", "factorial_dynamic")]
#[test_case("readme_example", "factorial")]
#[test_case("readme_example", "impl_trait" => ignore["`def <Trait> for <Class> where ...` external-implementation syntax and meta modifier not implemented"])]
#[test_case("readme_example", "list_shorthand")]
#[test_case("readme_example", "lists")]
#[test_case("readme_example", "mutability")]
#[test_case("readme_example", "pure_functions")]
#[test_case("readme_example", "sets_maps")]
#[test_case("readme_example", "total_functions" => ignore["`total` keyword not implemented"])]
#[test_case("readme_example", "trait_fin_meta" => ignore["meta/fin modifiers on trait methods not implemented"])]
#[test_case("readme_example", "trait_inheritance" => ignore["composing multiple parent traits not implemented"])]
#[test_case("readme_example", "traits" => ignore["generics on traits and external-implementation syntax not implemented"])]
fn syntax(input_dir: &str, file_name: &str) -> ParseResult<AST> {
let file_name = format!("{file_name}.mamba");
let source = resource_content(true, &[input_dir], &file_name).unwrap();
Expand Down
14 changes: 8 additions & 6 deletions tests/resource/valid/readme_example/builtin_trait.mamba
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ meta trait Measurable: Add, Sub, Eq, Comparable

# Built in to the standard library
# The idea is that this allows performing arithmetic not just at runtime but at compile-time.
def Measurable for Int where
def const less_than(self, other: Int) -> Bool := self < other
def const unary_sub(self) -> Int := -other
def const add(self, other: Int) -> Int := self + other
def const equal(self, other: Int) -> Bool := self = other
end
def Measurable for Int
# The following is already defined for Int, but for the sake of our example:
# {
# def meta less_than(self, other: Int) -> Bool := self < other
# def meta unary_sub(self) -> Int := -other
# def meta add(self, other: Int) -> Int := self + other
# def meta equal(self, other: Int) -> Bool := self = other
# }
Loading
Loading