Definition 1 (Asserted protocols) Asserted protocols, or just protocols for short, are
ranged over by S and are defined as the following syntax rules:
S ::=
|p.S action prefix
| +{ l_i : Si }_i∈I branching
| µt.S fixed-point
| t recursive variable
| end end
| assert(n).S assert (produce)
| require(n).S require
| consume(n).S consume
Process calculi are "fundamental" descriptions of computation analogous to lambda calculus but oriented around communication instead of function calls. (As far as paper structure, I find that usually the important "basic" definitions in programming language research papers are usually in Section 2, since Section 1 serves as a high-level overview). {
action1();
action2();
action3();
... rest_of_program ...
}
(in particular, making "the rest of the program" part of each statement makes specifying scope much easier and clearer, which is why they don't just use semicolons in the first place) int x; // not a const object, so can be freely modified
const int y; // is a const object, so cannot be modified
C/C++ pointer provenance doesn't include information about constness, so it doesn't matter "how" you got a mutable pointer to `x`: you're always allowed to modify `x` through that pointer, even if the pointer "came from" a const reference. int& upgrade_to_mut(const int& x) {
return *const_cast<int*>(&x);
}
int x = 5;
const int& x_ref = x;
int& x_ref_mut = upgrade_to_mut(x_ref);
x_ref_mut = 6;
it's only invalid if the object that is pointed at is const, as in: int& upgrade_to_mut(const int& x) {
return *const_cast<int*>(&x);
}
const int y = 5;
const int& y_ref = y;
int& y_ref_mut = upgrade_to_mut(y_ref); // it is actually legal to produce y_ref_mut, but we cannot modify it
y_ref_mut = 6; // this is UB: cannot modify a const object 'y'
The difference is that in Rust, "mutation capabilities" are part of references, and so you cannot create them out of nowhere, that would be UB. But in C++, mutation capabilities are part of the object being pointed at, so as long as they happen to be there when you perform the mutation (e.g. you're not modifying a string literal or a variable declared `const`) then there's no problem. fn example<'a>(r: &'a mut i32) -> &'static mut i32 {
unsafe { std::mem::transmute(r) }
}
fn main() {
let mut x: i32 = 5;
let ptr: &'static mut i32 = example(&mut x);
*ptr = 6;
println!("{x}");
}
(because it's unsound, it's considered wrong to do this - you should not intentionally write functions whose types are lies, and this one definitely lies, so it should be marked `unsafe` - but this is not automatic UB)
Like, append: Vector n t -> Vector m t -> Vector (n+m) t. With GADTs you need to manually lift (+) to work on the types, and then provide proofs that it's compatible with the value-level.
Then you often need singletons for the cases that are more complex- if I have a set of integers, and an int x, and I want to constrain that the item is in the set and is also even, the GADT approach looks like:
doThing :: Singleton Int x -> Singleton (Set Int) s -> IsInSet x s -> IsEven x -> SomeOutput
Which looks reasonable, until you remember that the Singleton type has to have a representation which is convenient to prove things about, not a representation that is convenient/efficient to operate on, which means your proofs will be annoying or your function itself will hardly resemble a "naive" untyped implementation.
Dependent types fix this because your values are "just" values, so it would be something like
doThing :: (x : int) -> (s : set int) -> so (is_in s x) -> so (even x) -> SomeOutput
Here, x and s are just plain values, not a weird wrapper, and is_in and event are ordinary (and arbitrary) library functions.