Skip to content

JSON null evades is nothing: Value::Null and Value::Nothing are crossed and never compare equal #651

Description

@logbie

Summary

WFL has two internal "no value" variants — Value::Null and Value::Nothing — and they are crossed:

  • the nothing literal evaluates to Value::Null (typeof reports "Null"), and x is nothing matches it;
  • parse_json maps a JSON null to Value::Nothing (typeof reports "Nothing"), and x is nothing does not match it.

Because PartialEq for Value short-circuits on std::mem::discriminant, the two variants never compare equal, so the natural guard check if x is nothing: silently passes JSON nulls through. The isnothing native has the opposite bias (matches only Value::Null), so it also misses parse_json nulls.

Reproduction

store payload as parse_json of "{\"k\":null}"
store v as payload["k"]
display "json-null typeof: " with typeof of v
check if v is nothing:
    display "json-null is-nothing: yes"
otherwise:
    display "json-null is-nothing: no"
end check

store w as nothing
display "literal typeof: " with typeof of w
check if w is nothing:
    display "literal is-nothing: yes"
otherwise:
    display "literal is-nothing: no"
end check

Observed (release build of current main):

json-null typeof: Nothing
json-null is-nothing: no
literal typeof: Null
literal is-nothing: yes

Expected: both branches say yes — a beginner writing check if v is nothing: after parse_json is expressing exactly this intent.

Where the pieces sit

  • src/stdlib/json.rsserde_json::Value::Null => Value::Nothing on parse; both variants serialize back to JSON null.
  • src/interpreter/value.rsimpl PartialEq for Value returns false for mismatched discriminants, so Null == Nothing is false; type_name() reports them as two different types ("Null" / "Nothing").
  • src/stdlib/core.rsnative_isnothing matches only Value::Null.
  • The nothing literal evaluates to Value::Null.

Impact

Real-world hit: a web handler that reads fields from a client JSON payload and guards them with is nothing lets {"api_key": null} straight through — in our case the null then reached an HTTP-client native and produced Expected text, got Nothing at runtime (line 0, column 0, so the failure doesn't even point back at user code). Untrusted-input validation is exactly where this guard gets written, which makes the silent pass-through worse than a crash at the check itself.

Workaround we're shipping in the app: type-based guards, e.g. check if typeof of model_id is not equal to "Text": — which catches both variants but is not the form a beginner would reach for first.

Suggested direction

Whatever the internal representation, the user-visible semantics should collapse to one concept:

  1. is nothing (and is not nothing) treat Value::Null and Value::Nothing as equal — likely in the equality path, with a special case ahead of the discriminant short-circuit.
  2. isnothing native matches both.
  3. typeof reports one name for both (today it leaks two internal names, and the names are swapped relative to intuition: the nothing literal reports "Null").

Happy to follow up with a failing-test PR (Red first, per testing.md) if that helps — the repro above drops straight into a TestPrograms/ case plus a unit test on Value equality.

Found while dogfooding WFL as the backend for a chat app (LogbieLLC/Rin app/server.wfl), same series as #647 and #648.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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