Coder Social home page Coder Social logo

Comments (18)

lhmouse avatar lhmouse commented on May 30, 2024

Unfortunately this would prevent proper tail calls. I doubt whether it is unavoidable. It is not an option to sacrifice PTC for this.

from asteria.

FrankHB avatar FrankHB commented on May 30, 2024

You have to trade off here. Scoped cleanup alters the tail context. This will always have semantic consequences.

from asteria.

FrankHB avatar FrankHB commented on May 30, 2024

Theoretically the only possible ones (without changing semantic rules) are idempotent effects provable having no more footprint than constant space. For example, preserving a flag in each tail call frame to indicate some fixed cleanup always having same behavior after called (once or more) will still allow the behavior effectively PTC because recursive calls won't acquire more space for the activated frames. Arbitrary actions specified in defer or finally are never in this kind.

Racket may have similar problems in contracts handling. I still don't have explored more possibilities of the solution yet.

from asteria.

lhmouse avatar lhmouse commented on May 30, 2024

You have to trade off here. Scoped cleanup alters the tail context. This will always have semantic consequences.

Deferred functions can be packed into the tail call argument structure so I presume it's not impractical.

The most confusing thing is actually the syntax. Given defer cleanup(foo(42)); then which part is deferred? It could be explained as the entire expression is deferred, or foo(42) is evaluated immediately but the call to cleanup() is deferred (which results in inconsistency), or the entire expression is evaluated whose result is hypothesized to be a function taking no arguments which is deferred.

from asteria.

FrankHB avatar FrankHB commented on May 30, 2024

Deferred functions can be packed into the tail call argument structure so I presume it's not impractical.

Is it allowed to allocate here?

The most confusing thing is actually the syntax. Given defer cleanup(foo(42)); then which part is deferred?

Yep, this is one of the problem, but boring. The simple way is to just defer anything visually possible to defer, i.e. cleanup(foo(42)) in your example. If this is not user want, foo(42) can be lifted out and placed before the defer statement. That's it.

One obvious reason to have syntax like defer is its simplicity allows idiomatic use. If the users have to specify different configurations of which part within the syntax to defer, it won't be much useful. This also suggests that the simple way is better.

But more generally, there is also the problem to decide the destination: when should cleanup be actually called? The trivial naive choice is "to defer to any time as you like", which makes it essentially non-deterministic, which is in turn not useful for cleanup. This severely undermines the necessity of constructs like defer for idiomatic use.

from asteria.

lhmouse avatar lhmouse commented on May 30, 2024

Deferred expressions are evaluated at scope exits. As variables may be re-declared (later-declared names hide earlier-declared ones) the expression must be rebound at the point of declaration. (Then what would defer i++; do? It's sort of cryptic and misleading.) We have support of nested exceptions so if deferred expressions throw exceptions they are caught and rethrown, but this can't be done in destructors (i.e. RRID would not be an option).

from asteria.

FrankHB avatar FrankHB commented on May 30, 2024

Deferred expressions are evaluated at scope exits.

This does not necessarily mean that the actual effects of the evaluations are bound at scope exits. Consider memory deallocation: even C++ is free to defer the effects of global deallocation function calls once proven as-if, because the deallocations are not considered observable behaviors in the language. However, once you want the concerned behaviors explicit in the semantics (e.g. the guarantee of PTC), it is the rule-breaker. You will eventually need more rules to override the default ones to make the optimization over uninterested behaviors possible again.

As variables may be re-declared (later-declared names hide earlier-declared ones) the expression must be rebound at the point of declaration. (Then what would defer i++; do? It's sort of cryptic and misleading.)

Sounds unclear to me. By naming the feature "hiding", it introduces more names, leaving the hidden ones unchanged. The canonical way to implement local blocks (let transformation) always introduce more blocks when you separately declare new names, and there will be no ambiguities among the scopes involved once you know where the scopes end (either by explicit syntactic block ending marks like }, or implicit rules to insert such marks at the innermost explicit block ending marks). The only frustration in your case indicates direct reusing of the local context, hence environment modification. This is different to name hiding.

We have support of nested exceptions so if deferred expressions throw exceptions they are caught and rethrown, but this can't be done in destructors (i.e. RRID would not be an option).

This is another idiomatic use which out of the scope of resource cleanup. "Defer" can be still the right name, and RRID is certainly not. (It is actually more close the the semantics of the current C++ possibly throwing destructors, though.) The problem is it even interferes more against PTC. I don't think you can have anything meaningful here before you know the deferred evaluations have precise properties (i.e. the space complexity constraints) consistent to PTC.

from asteria.

FrankHB avatar FrankHB commented on May 30, 2024

Why would you defer i++??
Wasn't i++ equal to:
auto temp(i);
++i;
returrn temp;
???

I don't expect it sane, but to rule out such use is also not trivial, besides explicitly being "unspecified". This is essentially the same kind of work to shape the language rules allowing proving it consistent with the PTC guarantee.

from asteria.

lhmouse avatar lhmouse commented on May 30, 2024

Deferred expressions are evaluated at scope exits.

This does not necessarily mean that the actual effects of the evaluations are bound at scope exits. Consider memory deallocation: even C++ is free to defer the effects of global deallocation function calls once proven as-if, because the deallocations are not considered observable behaviors in the language. However, once you want the concerned behaviors explicit in the semantics (e.g. the guarantee of PTC), it is the rule-breaker. You will eventually need more rules to override the default ones to make the optimization over uninterested behaviors possible again.

The invariant of Asteria is that values may be copied or erased with no side effects. So memory deallocation is required not to have any side effect

The only trade-off is that deferred expressions might not be executed at all in case of initialization failures of its context, due to failure to allocate memory for example.

As variables may be re-declared (later-declared names hide earlier-declared ones) the expression must be rebound at the point of declaration. (Then what would defer i++; do? It's sort of cryptic and misleading.)

Sounds unclear to me. By naming the feature "hiding", it introduces more names, leaving the hidden ones unchanged. The canonical way to implement local blocks (let transformation) always introduce more blocks when you separately declare new names, and there will be no ambiguities among the scopes involved once you know where the scopes end (either by explicit syntactic block ending marks like }, or implicit rules to insert such marks at the innermost explicit block ending marks). The only frustration in your case indicates direct reusing of the local context, hence environment modification. This is different to name hiding.

Consider:

var a = "hello";
func get_a() { return& a;  }
var a = 42;
std.debug.print("a = $1", a);  // prints the second `a`, which is `42`
std.debug.print("get_a() = $1", get_a());  // prints the first `a`, which is "hello"

From a low-level point of view, the later-declared reference with name a overwrites the earlier-declared one, but the variable that it references is left intact.

This is purely for convenience. Requiring statements consisting purely of assignment expressions to have distinct grammar construction from definitions with initialization isn't sort of nice.

We have support of nested exceptions so if deferred expressions throw exceptions they are caught and rethrown, but this can't be done in destructors (i.e. RRID would not be an option).

This is another idiomatic use which out of the scope of resource cleanup. "Defer" can be still the right name, and RRID is certainly not. (It is actually more close the the semantics of the current C++ possibly throwing destructors, though.) The problem is it even interferes more against PTC. I don't think you can have anything meaningful here before you know the deferred evaluations have precise properties (i.e. the space complexity constraints) consistent to PTC.

As mentioned in the first paragraph, scope exits cannot have other side effects other than deferred expressions. If we pack deferred expressions as PTC arguments then we can evaluate them after every PTC (note PTCs can be chained).

from asteria.

FrankHB avatar FrankHB commented on May 30, 2024

This is purely for convenience. Requiring statements consisting purely of assignment expressions to have distinct grammar construction from definitions with initialization isn't sort of nice.

No, that is the headache. Do avoid that py-ish™ stuff.

from asteria.

FrankHB avatar FrankHB commented on May 30, 2024

I think C++ should standardize this rule for basic types and allow operator(int)=default or whatever.

Basically I agree, but this is out of the scope. (ISO C++ has requirements on types like InputIterator. For historical reasons, this can't be reasonably enforced.) For a fresh language design, I prefer not to have the postfix ++ at all. But this still not resolves the problem, because other constructs can also have side effects and they must be ruled out for sanity, too.

from asteria.

lhmouse avatar lhmouse commented on May 30, 2024

This is purely for convenience. Requiring statements consisting purely of assignment expressions to have distinct grammar construction from definitions with initialization isn't sort of nice.

No, that is the headache. Do avoid that py-ish™ stuff.

Virtually the only difference between

var a = foo();
bar(a);
var a = foo();
bar(a);

and

var a = foo();
bar(a);
a = foo();
bar(a);

is that the former actually creates two variables while the latter introduces only one variable. The difference can be observed if the name a is captured by a closure e.g. inside func capture_a() { return a; } but in this snippet it is trivial.

from asteria.

FrankHB avatar FrankHB commented on May 30, 2024

Virtually the only difference between

This is not about the ease to see the visual difference. It is about the fact that it screws up many kinds of semantic reasoning in unexpected manners, which can even render the specification of the language (if any) almost useless here. This is exactly the lesson you'd better learned from languages like Python, see this example (in zh-CN) for example.

from asteria.

lhmouse avatar lhmouse commented on May 30, 2024

Virtually the only difference between

This is not about the ease to see the visual difference. It is about the fact that it screws up many kinds of semantic reasoning in unexpected manners, which can even render the specification of the language (if any) almost useless here. This is exactly the lesson you'd better learned from languages like Python, see this example (in zh-CN) for example.

What Python does is that assignments become definitions implicitly. We do not do that. Assignments (compound or simple) don't become definitions. We have been aware of that simple assignments destroy the contents of destination variables, so it is acceptable to substitute simple assignments with definitions but not the other way around.

from asteria.

FrankHB avatar FrankHB commented on May 30, 2024

So this is saner than Python, but still confusing. As per the traditional meaning of the block scope, a block creates a fresh environment. Once there is a scope exit, the environment is dropped. If a redeclaration of variable that has already declared implies hiding, there is an expectation of equivalence between:

var a = foo();
bar(a);
var a = foo();
bar(a);

and

var a = foo();
bar(a);
{
var a = foo();
bar(a);
}

Which have more differences beyond how many variables being created.

from asteria.

lhmouse avatar lhmouse commented on May 30, 2024

So this is saner than Python, but still confusing. As per the traditional meaning of the block scope, a block creates a fresh environment. Once there is a scope exit, the environment is dropped.

Yes. I presume that, as described in the OP, such deferred callbacks are called during PTC unpacking, which means they execute in rebuilt contexts rather than the same context where they were created. There could be unexpected effects though.

from asteria.

FrankHB avatar FrankHB commented on May 30, 2024

Yes. I presume that, as described in the OP, such deferred callbacks are called during PTC unpacking, which means they execute in rebuilt contexts rather than the same context where they were created. There could be unexpected effects though.

OK, if this is just the remaining problem... I'd say that's why I'm not fond of implicit blocks and ALGOL-like syntax of blocks which does not emphasize the scope or merely marks the boundary of the blocks visually significant. Generally, blocks are essentially context-sensitive (as lambda abstractions do), but in those curly-braced languages, they are pretended to be not. This only works when the language is lacking of the accessibility to the contexts and the identification of contexts is uninterested. This is certainly not the case where more powerful features are needed. PTC is just very basic one of such features.

Enforcing constructs like let {} instead of plain {} or implicit {} will eliminate the problem: no let {} implies no blocks, and defer can just do its owned sane things. Indentation problem will be another story, though.

from asteria.

lhmouse avatar lhmouse commented on May 30, 2024

defer should be working now in the non-PTC case (59e90fd).

In order for defer to work with proper tail calls (we must be able to evaluate deferred expressions), a context is required. In order to create a context we need to create a zero-ary argument getter, which is due to our early assumption that all statements and expressions execute as part of a function, while during tail call expansion, such contexts are destroyed before returning from the callee.

However, all arguments will have been bound by then. So it might be possible that we pass a null pointer as the argument getter which should never be used.

Another solution will be having the tail call argument struct carry the zero-ary argument getter from the callee, guaranteeing that all PTC'd contexts have correct, valid zero-ary argument getters. The corollary is that now we have to keep an indefinite number of contexts and disposal of deferred expressions becomes much nasty, owing to the fact that we can't just bail out on exceptions (deferred expressions are always evaluated even another exception is thrown) so we are forced to catch and stash them.

from asteria.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.