Skip to content

CEP-0068: Value-Producing Local Declarations

Draft

Draft proposal to reconsider whether local declaration statements can complete with their bound value. V1 keeps declaration statements non-expression forms; block values come from the last expression, and local declaration completion is not accepted as a value-producing shortcut.

Summary

Catalyst should revisit whether a local declaration statement such as const x = expr may complete with the value of its binding when it appears as the last statement in a block.

Current V1 behavior stays conservative: declarations introduce bindings, but they are not expressions. A block value comes from its last expression, and a block that should produce a local binding's value must spell that value explicitly.

Motivation

The shorthand can be attractive in small scoped computations:

const value = {
  const x = compute()
}

If local declarations were value-producing, this could mean that the block evaluates to x. That may read naturally when the declaration is a simple immutable binding and the initializer is the value the programmer wants to return.

The current accepted form is more explicit:

const value = {
  const x = compute()
  x
}

This proposal preserves the question because the shorthand may fit Catalyst's expression-oriented block model, but it should not be adopted without deciding copy, move, resource, mutability, and diagnostic behavior.

Proposed Direction

Possible future direction:

  • a local const declaration statement may complete with the initialized binding value;
  • the completed value is checked with the same copy/move/borrow rules as an explicit final expression naming that binding;
  • a local var declaration either completes as void or is rejected as a value-producing completion unless the source explicitly names the binding after the declaration;
  • destructuring declarations complete only when the rule can define a single obvious value, likely the original initialized value rather than one of the destructured bindings;
  • declarations whose initializer produces a non-copy resource must not accidentally move the binding out of scope unless the source makes that move explicit.

This is not accepted V1 behavior. The proposal needs a crisp rule for which declaration forms produce values and how ownership is represented.

Example

Possible future shape:

const answer = {
  const x = 41
}

If accepted, this could make answer equal to 41.

The accepted V1 spelling remains:

const answer = {
  const x = 41
  x
}

Ownership Questions

The hard cases are the reason this proposal stays deferred:

const file = {
  const f = open_file()
}

The language must decide whether the declaration completion copies f, moves f, borrows f, or is invalid for non-copy values. A hidden move would be surprising; a hidden copy may be impossible; a hidden borrow may outlive the block.

For mutable bindings:

const value = {
  var x = compute()
}

The proposal must decide whether var can complete with the assigned value, whether this exposes the mutable storage value by copy, and whether mutation after initialization affects the completion model.

V1 Compatibility

V1 remains unchanged:

  • declarations are not expressions;
  • local declaration statements introduce bindings and declaration facts;
  • a block's value is its last expression;
  • an empty block evaluates to void;
  • code that wants a local declaration's value as a block result writes the binding as an explicit final expression.

Accepting this CEP would be a source-compatible relaxation for programs that currently fail because a block ending in a declaration cannot satisfy a non-void expected type. It may also change lint expectations around redundant final binding expressions, so acceptance should coordinate with the lint catalog.

Open Questions

  • Should only const declarations be value-producing, or should var declarations participate too?
  • Is the completion value the initialized expression value or the declared binding value after annotation/coercion?
  • How does the rule interact with destructuring declarations?
  • What happens for non-copy and resource-owning values?
  • Should an explicit move x still be required when declaration completion would otherwise move out of a local?
  • Should declaration completion be allowed only in final block position?