What I did
Put these three files in one directory and check main.bend.
law.bend:
law k:
for ~B.x: Data
Data
law.k~B.bend:
import ./law.bend as L
type Empty is Data:
type Bit is Data:
False{}
True{}
type Marker is Data:
x{}
def L.k(B.x):
x
type Bad is Type:
MkBad{apply: @+b: L.k(~Empty) -> Empty}
def x() -> Type:
Bad
def Bad.apply(b: Bad) -> (@+x: L.k(~Empty) -> Empty):
match b:
case MkBad{f}:
f
def step(+b: L.k(~Empty)) -> Empty:
Bad.apply(b)(MkBad{Bad.apply(b)})
def boom() -> Empty:
step(MkBad{step})
def absurd(-A: Type, e: Empty) -> A:
match e:
def false_theorem() -> {False{} == True{} : Bit}:
absurd({False{} == True{} : Bit}, boom())
main.bend:
import ./law.k~B.bend as E
def boom() -> E.Empty:
E.boom()
Then run:
bun bend2/main.ts main.bend --check-only
What happened
The accepted safe book has no holes, open laws, @unsafe definitions, FFI, or Base import. It contains the closed boom() -> Empty and derives False{} == True{}.
Why it happens
The imported module's namespace is law.k~B. Its constructor x{} therefore owns the constructor key law.k~B.x. The fill body x resolves through that constructor-table entry to Ref("law.k~B.x"). Definitions and constructors use separate freshness checks, so the later def x() -> Type is also admitted under the TLD key law.k~B.x. parse_reso parse_fresh
book_valid hides later TLDs and reveals declarations in order. When it checks the earlier fill of law.k, the constructor is visible but the later TLD law.k~B.x is not. def_check constructs exactly that name for the opaque ~B.x parameter and tests freshness only in the currently visible TLD table. The test therefore passes, the opaque temporarily shadows the constructor key, and the body checks as Data. def_check book_valid
Validation then reaches the later def x() -> Type and installs it under the same key. From that point, reducing the already checked body of L.k resolves its Ref("law.k~B.x") to this definition and yields Bad, which is Type-kinded. The declared result of L.k is still Data, so L.k(~Empty) licenses the + binder over the negative Bad type. The ordinary Curry term in step then inhabits Empty.
This is a bypass of the opaque-name freshness guard added for #905. Moving the later def x before the law fill makes that guard reject the opaque name as expected; leaving it later keeps it outside the visible TLD table during generic checking.
Expected behavior
An opaque template parameter should have an identity that cannot be captured by constructor or definition names, including declarations revealed later. At minimum, constructor and TLD keys should not be reusable in this order-sensitive way, and opaque-name freshness should cover the complete namespace rather than only the currently visible TLD table.
Version
bend 2.0.25
commit a49524265bdfa5753a4bf38e25f0574a705dd868
Bun 1.4.2
Darwin arm64
What I did
Put these three files in one directory and check
main.bend.law.bend:law.k~B.bend:main.bend:Then run:
What happened
The accepted safe book has no holes, open laws,
@unsafedefinitions, FFI, or Base import. It contains the closedboom() -> Emptyand derivesFalse{} == True{}.Why it happens
The imported module's namespace is
law.k~B. Its constructorx{}therefore owns the constructor keylaw.k~B.x. The fill bodyxresolves through that constructor-table entry toRef("law.k~B.x"). Definitions and constructors use separate freshness checks, so the laterdef x() -> Typeis also admitted under the TLD keylaw.k~B.x.parse_resoparse_freshbook_validhides later TLDs and reveals declarations in order. When it checks the earlier fill oflaw.k, the constructor is visible but the later TLDlaw.k~B.xis not.def_checkconstructs exactly that name for the opaque~B.xparameter and tests freshness only in the currently visible TLD table. The test therefore passes, the opaque temporarily shadows the constructor key, and the body checks asData.def_checkbook_validValidation then reaches the later
def x() -> Typeand installs it under the same key. From that point, reducing the already checked body ofL.kresolves itsRef("law.k~B.x")to this definition and yieldsBad, which isType-kinded. The declared result ofL.kis stillData, soL.k(~Empty)licenses the+binder over the negativeBadtype. The ordinary Curry term instepthen inhabitsEmpty.This is a bypass of the opaque-name freshness guard added for #905. Moving the later
def xbefore the law fill makes that guard reject the opaque name as expected; leaving it later keeps it outside the visible TLD table during generic checking.Expected behavior
An opaque template parameter should have an identity that cannot be captured by constructor or definition names, including declarations revealed later. At minimum, constructor and TLD keys should not be reusable in this order-sensitive way, and opaque-name freshness should cover the complete namespace rather than only the currently visible TLD table.
Version