BlockstreamResearch / simfony

Rust-like high-level language that compiles down to Simplicity bytecode. Work in progress.
19 stars 6 forks source link

Closures #21

Closed uncomputable closed 4 months ago

uncomputable commented 5 months ago

Add closure expressions that return a function value.

The Simfony type system cannot handle function types, so we introduce a separate let-statement which only handles closures. See #20

Closures capture environment variables by name, which can lead to confusion:

let a = 1;
let f = || a;
jet_verify(jet_eq_32(1, f()));
let a = 2;
jet_verify(jet_eq_32(2, f()));

Changing environment variables implicitly changes the closure output. This is impossible in Rust.

Closures should capture environment variables by value. We can look into this when we reform the environment itself, which currently is a product that gets fatter with each let-statement.

apoelstra commented 5 months ago

Why do these functions capture their environment at all?

apoelstra commented 5 months ago

Why do we use let syntax for a variable binding that doesn't insert a variable into the environment? This seems like it would just confuse users who would expect to be able to write let f = || whatever and then use f in a variable context.

uncomputable commented 5 months ago

List folds and loops tend to read from the environment in each iteration. It helps to differentiate mutable state / accumulators from the immutable environment. This is why I implemented closures instead of functions.

Currently, closures capture variables by name, which is broken. In the future, closures will capture variables by value and the environment will be properly immutable.

In a list fold or loop, the environment stays constant across all iterations.

The second question boils down to the first question: If we use functions, we can use function syntax. If we use closures, we need some sort of let statement.

Simfony doesn't support function types, so there is a separate let statement just for closures; a dirty hack. With two let statements, some identifiers include values and some include closures. We cannot use value identifiers in closure contexts and vice versa. This is potentially confusing.

In the end, I'm emulating Rust's syntax and type system. In Rust you can assign a value and a closure to the same identifier with no problem. I have to fix the GlobalScope a little bit, but if we always use the latest binding to resolve an identifier (value or closure), then there should be no confusion.

let x = 1u32;
let x = || { x };
assert_eq!(1, x());
apoelstra commented 5 months ago

List folds and loops tend to read from the environment in each iteration. It helps to differentiate mutable state / accumulators from the immutable environment. This is why I implemented closures instead of functions.

Ok, this is somewhat reasonable (though I don't really agree with it). But you still shouldn't be using the let keyword for something that does not actually bind a value to a variable.

Currently, closures capture variables by name, which is broken. In the future, closures will capture variables by value and the environment will be properly immutable.

In a list fold or loop, the environment stays constant across all iterations.

Yes, if closures captured values rather than variable names, the situation would be much less confusing. Though still IMHO more complicated than it needs to be.

The second question boils down to the first question: If we use functions, we can use function syntax. If we use closures, we need some sort of let statement.

I'm confused. The difference between a closure and a function is that a closure can capture parts of its environment. The difference between let and non-let syntax is that let creates a variable binding. The two things are completely unrelated.

Simfony doesn't support function types, so there is a separate let statement just for closures; a dirty hack. With two let statements, some identifiers include values and some include closures. We cannot use value identifiers in closure contexts and vice versa. This is potentially confusing.

It's extremely confusing to use the same keyword to mean unrelated things.

In the end, I'm emulating Rust's syntax and type system. In Rust you can assign a value and a closure to the same identifier with no problem.

Yes, but in Simfony you cannot assign closures to anything because your magic let syntax does not create a variable binding at all.

I have to fix the GlobalScope a little bit, but if we always use the latest binding to resolve an identifier (value or closure), then there should be no confusion.

let x = 1u32;
let x = || { x };
assert_eq!(1, x());

In Rust you can use the value x after assigning a closure to it. In your code you cannot. Yes, in Rust you can shadow declarations. But this (again) is unrelated to closures.

uncomputable commented 5 months ago

Do you suggest to use closure literals in things like loops? This would avoid the let statements.

Example:

let list = list![1, 2, 3, 4, 5, 6, 7];
let sum = fold::<8>(list, 0, |el, acc| {
    let (carry, sum) = jet_add_32(el, acc);
    // Check carry =? 0
    jet_verify(jet_complement_1(carry));
    sum
});
let expected = 28;
jet_verify(jet_eq_32(expected, sum));

What is non-let syntax?

apoelstra commented 5 months ago

Do you suggest to use closure literals in things like loops?

What is the signature of fold here? AFAICT you cannot define such a function in Simfony, whether you are using a literal or a named closure.

But yes, if you can use let with a closure expression then you should be able to use use closure expressions anywhere else.

What is non-let syntax?

For example you could add a fn keyword, or maybe a closure keyword if you want to be clearer.

uncomputable commented 4 months ago

I agree that introducing closures as a kind of expression that cannot be used everywhere is confusing.

Loops need closures, but we can use literals. Outside of loops, we can use pure functions with the fn syntax.

fold::<N> has the signature List<El, N> → Acc → (El → Acc → Acc) → Acc. There is an environment for readonly variables, so maybe the signature becomes something like Env → List<El, N> → Acc → (Env → El → Acc → Acc) → Acc. Let's discuss the detailed implementation at #22 once we get to it.

uncomputable commented 4 months ago

Converted to draft until the implementation is closer to what we want.

uncomputable commented 4 months ago

Closed in favor of #24