Skip to content

rainlang is untyped, so whether a word can validate its own operand is an accident of the encoding #550

Description

@thedavidmeister

Rainlang is untyped, so a word receives raw stack bits and has no way to know what they were meant to be. Whether a word can check its own operand is an accident of that operand's encoding, not a property of the language — and where the encoding carries no tag, the check is not merely unimplemented but impossible.

This is filed as the language-level home for a finding that keeps recurring per-word and per-repo with no general answer.

The concrete case

rainlanguage/rain.dia#40 asked LibOpDiaPrice.run to validate that inputs[1] (staleAfter) is a legitimate Float before toFixedDecimalLossless reads it, proposing LibDecimalFloat.isFloat(staleAfter).

It cannot be built as specified:

  • Float is a bare alias — type Float is bytes32 — and unpack() is bit reinterpretation with no discriminant and no failure mode. Every 256-bit word is a valid Float. There is no representation to validate.
  • LibDecimalFloat.isFloat exists in no version of rain.math.float, pinned or on main.

Contrast the sibling finding on input 0. V3 IntOrAString carries a 0xE0 type tag in its low byte, so feedKey can be checked, and was. That is the whole asymmetry: one operand is checkable because its encoding happens to reserve bits for a tag, the other is not because it does not. Neither fact is a decision anyone made about the word.

Why this does not belong in the words repos

  • It recurs identically across them. rain.pyth's LibOpPythPrice.run assembly-loads inputs[1] and calls toFixedDecimalLossless(staleAfter, 0) byte-identically. Any future oracle word will do the same.
  • A repo-local runtime guard would diverge the family. Adding a bound in rain.dia and not rain.pyth makes two words that are meant to be the same word behave differently, and the divergence would be invisible from either repo.
  • The only implementable guard is semantic, not representational — a maximum staleAfter bound. That is a different thing from what the audit asked for, and choosing a number is a design decision about the words family rather than a fix to a validation gap.

So the per-word answer is either "impossible as asked" or "invent a policy", and neither is a bug fix. The place where an operand's intended type could actually be known is the compiler.

What this issue is asking

Whether rainlang should carry operand types at all, and if so at what granularity — that is a language design question, not a defect report, and it is the reason this is filed here rather than as a fix.

Worth settling rather than assuming:

  • Granularity. Literal types (#218 is adjacent — 1_d6 / 1_d0 / 1_u), operand types on word signatures, stack-slot types carried through the expression, or some subset. These have very different costs.
  • What existing rainlang does. Every expression written to date is untyped by construction. Whether typing is opt-in, inferred, or a break is the question that decides whether this is buildable at all.
  • Whether the interpreter still needs runtime checks. A type system that only runs at parse time is a tool for authors. It is worth being explicit that it would not, on its own, replace anything.

The limit this does NOT clear, and it should be stated up front

Compile-time typing does not guard a direct external call. DiaWords.extern is publicly callable on-chain; a caller invoking it directly never goes through the parser, so no amount of language-level typing constrains the bits they push. On the specific case that prompted this, that exposure is bounded — the extern is view and stateless, so a caller passing a nonsense staleAfter gets a stale price returned to itself, and toFixedDecimalLossless already reverts on everything rejectable (NegativeFixedDecimalConversion, FixedDecimalOverflow, LossyConversionFromFloat), leaving an accepted set that is exactly the non-negative-integer-seconds domain staleAfter is defined over.

So the honest scope is: typing would catch authoring mistakes at compile time. It would not make a word safe against an adversarial direct caller, and any word whose correctness depends on that still needs its own guard. If the answer to this issue is "rainlang stays untyped", that is a legitimate outcome and it makes the per-word findings wont-fix on a stated principle rather than one at a time.

Related

  • rainlanguage/rain.dia#40 — the finding that prompted this; impossible as specified, with the full analysis on the issue
  • rainlanguage/rain.pythLibOpPythPrice.run, the byte-identical sibling that would diverge under a repo-local fix
  • #218 — types in decimal literals, adjacent granularity question

Metadata

Metadata

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