Skip to content

push with CONST and value mutates a constant list — expression write-targets escape the constness check #673

Description

@logbie

Follow-up to #671, split out because it needs a different fix.

#671 closed the constness hole for the three bare-name mutation statements — add ... to, remove ... from, and clear — which carry their target as a list_name: String the analyzer can resolve directly. push does not: Statement::PushStatement carries list: Expression (src/parser/ast.rs:453), so the same check has nothing to look up, and a constant list can still be mutated.

Repro

store new constant ROLES as ["admin"]
push with ROLES and "guest"
display ROLES
[admin, guest]
exit 0

No analyzer report, no runtime error. Verified against the #671 fix on branch claude/issue-671-8ufs93 — that change does not affect this path.

Nested targets go the same way:

store new constant CONFIG as [["a"]]
push with CONFIG[0] and "b"
display CONFIG          // [[a, b]]   exit 0

Why it escapes both layers

Analyzer — the PushStatement arm only walks its two sub-expressions; there is no write-target concept for an expression:

// src/analyzer/mod.rs:2358
Statement::PushStatement { list, value, .. } => {
    self.analyze_expression(list);
    self.analyze_expression(value);
}

Interpreter — push evaluates the target to a Value::List and mutates through the Rc<RefCell<Vec<Value>>> in place (src/interpreter/mod.rs:8942). The binding is never reassigned, so Environment::assign — the only place the constant flag is enforced at runtime — is never reached. This is the same in-place-mutation escape #671 described for add/remove/clear on constant lists; there, catching it during analysis was enough because the target was a bare name.

What a fix needs

Resolving a write target through an expression, at minimum for the Expression::Variable and index/member chains that bottom out in one:

  • push with ROLES and x → root binding ROLES
  • push with CONFIG[0] and x → root binding CONFIG
  • push with (some_call with y) and x → no root binding, nothing to report

The analyzer already tracks declared constants precisely in constant_bindings (added in #671), so the missing piece is the root-binding walk, not the constness test.

Related but distinct: aliasing defeats constness entirely

Worth recording here since it bounds how much a push fix buys, but it is a separate problem and should not be folded into this one:

store new constant ROLES as ["admin"]
store alias as ROLES
add "guest" to alias
display ROLES           // [admin, guest]   exit 0

alias is a legitimately mutable binding, so no write-target analysis can reject this. It shares the underlying Rc with the constant. Closing it means deciding what constant means for reference values — copy-on-bind, a deep freeze flag on the value, or an explicit documented "constants fix the binding, not the contents". That is a language-design question, not a missing check.

Acceptance criteria

  • push with <constant list> and <value> reports Cannot modify constant '<name>' during semantic analysis.
  • Indexed and member targets rooted at a constant (push with CONST[0] and x) report the same.
  • Push targets with no root binding (a call result, a literal) are unaffected.
  • Mutable bindings, action and container-method parameters, and loop variables are unaffected — the Analyzer drops the add ... to CONST report when combined with other constant mutations #671 negative tests in tests/constant_mutation_analyzer_test.rs cover the shape these must not regress into.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions