diff --git a/doc/reference.md b/doc/reference.md index 66f403adf6..774f2638dd 100644 --- a/doc/reference.md +++ b/doc/reference.md @@ -114,7 +114,7 @@ true // Boolean literal false // Boolean literal () // Unit literal (empty tuple/record) null // Null -undefined // Undefined +undefined // Undefined: an unspecified value (see Runtime Representations) ``` **String escape sequences:** @@ -758,10 +758,15 @@ Modules cannot extend classes. Objects can extend classes. ```mlscript {a: 1, b: 2} // immutable (frozen) record -mut {a: 1, b: 2} // mutable record +{mut a: 1, b: 2} // record whose field `a` is mutable +mut {a: 1, b: 2} // mutable record: all its fields are mutable (a: 1, b: 2) // record in parentheses (same thing) ``` +Only mutable fields can be assigned. Fields are immutable unless they are declared `mut` or +their whole record is mutable. A mutable record also makes mutable the fields it copies from +the records it spreads. A record spreading another one copies the current values of its fields. + ### Field Access ```mlscript @@ -1543,6 +1548,80 @@ candidate and a known candidate with an invalid operation, the known error is still reported. Dynamic values also do not remove the need for an unambiguous class or type identity in constructor patterns, class projections, or type references. +### Runtime Representations + +Compilation chooses a runtime *representation* for each slot that holds values: parameters, +results, fields, and variables. On the JavaScript backend, all values share one representation, +so representations mostly matter on the Wasm backend, where an `Int32` is an unboxed `i32` and +a class instance is a reference to the class's struct. A type annotation determines the +representation of its slot. With new resolution (`#lang(0.3.x)`), an unannotated slot takes the +representation of the values that resolution observes flowing into it: + +```mlscript +private fun roundtrip(value) = value // `value` and the result are represented as `Int32` +roundtrip(wasm.i32.const(42)) +``` + +A value is converted to the representation of each slot it flows into. Converting between +unrelated representations is a compilation error, whether the slot's representation is declared +or inferred, and so is a slot whose values have no common representation. This also rejects +programs whose only problem is a misleading annotation, which the JavaScript backend would run: + +```mlscript +class Tree +class App(callee: Tree, argument: Tree) +fun mk(t: Tree) = App(t, 1 :: Nil) // error: a list is used at the representation of `Tree` + +let value = wasm.i32.const(1) +set value = wasm.i64.const(2) // error: `Int32` and `Int64` have no common representation +``` + +Slots that can receive values from outside the compilation unit admit them: the unannotated +parameters of exposed definitions (see Resolution Interfaces) are inferred with unknown inputs. +Worksheet blocks are compiled one after the other, so without `#lang(strictResolution: true)`, +later blocks must use an earlier block's definitions with the representations inferred from that +block's own uses. + +A mutable field holds its initializer and then the values assigned to it, through any receiver that +can be an instance of its class, so an unannotated mutable field takes the representation of all of +them. Since other compilation units can assign values to the mutable fields that a compilation unit +exposes, those must declare their types, as their representations must not depend on the values that +the compilation unit happens to assign to them: + +```mlscript +private class Cell(mut val value) // `value` is represented as `Int32` +private fun reset(cell) = set cell.value = wasm.i32.const(0) +reset(Cell(wasm.i32.const(1))) + +class Counter(mut val count) // error: `count` is exposed, so its type must be declared +class Counter(mut val count: Int) // ok +``` + +Mutable record fields (see Record Literals) can be assigned too, but cannot declare their types: +when exposed, they take the unknown representation. Through a record type declaring a mutable +field, the values it describes can be assigned values of that field's input type, which their own +fields therefore admit. Exposing a record also exposes values stored by assignments, including +through fields copied by a mutable spread: those values can be read by clients just like initializers. + +An override shares the representations of the member it overrides. Its unannotated parameters +take the inherited parameters' representations, its annotated parameters must admit them (and, +on the Wasm backend, which compiles overrides with the overridden method's signature, match +them), and its results must fit the inherited result. A mismatch is a compilation error. Since +subclasses in other compilation units must implement an open member with its representations, +an open member whose parameters receive values from outside the compilation unit must annotate +the types of its parameters and result. + +The builtin operators are not yet declared as overloaded functions: their results are approximated +from their operands. For example, the sum of two `Int`s is an `Int` (an overflow to `Infinity` is +not detected), and `+` with a `Str` operand is a `Str`. On the Wasm backend, which implements the +operators with dynamically typed runtime functions, their results are treated as unknown values. + +`undefined` denotes an unspecified value of any representation. It can be stored in or passed to +any slot, but reading it back is undefined behavior: the JavaScript backend produces JavaScript +`undefined` (or `()` for a function returning it), while the Wasm backend may produce any value +of the slot's representation, such as `0` for an `Int32` or a null reference. Compiler-generated +placeholders, such as the arguments supplied for eliminated dead parameters, rely on this. + ### Generic Type Parameters Covariant, contravariant, invariant: @@ -1609,7 +1688,26 @@ lookup still combines the fields of record intersections. ```mlscript type Point = { x: Num, y: Num } -``` +type Cursor = { mut position: Int } +type CursorView = { mut position: in Int out (Int | Str) } +``` + +The values that a record type describes can be assigned values of the input types of its mutable +fields through it, but not values of its other fields. In `{ mut f: in S out T }`, writes accept `S` +and reads expose `T`. Omitting `in` gives `Nothing` as the input; omitting `out` gives `Any` as the +output. `{ mut f: U }` uses `U` in both positions, while `{ f: U }` has only a read interface. + +Field outputs are covariant and field inputs are contravariant. Thus `{ mut f: S } <: { mut f: T }` +contributes both `S <: T` for reads and `T <: S` for writes; `mut { f: S }` follows the same rule. +The two occurrences are interpreted separately before substituting type arguments: +for `type Cell[A] = { mut f: A }`, `Cell[in S out T]` exposes +`{ mut f: in S out T }`, not a single invariant occurrence of either bound. Enclosing negative +positions reverse these choices, just as for function inputs. Intersecting record interfaces +intersects their shared fields' output types and unions their input types. Assigning a callback +through a mutable field constrains it against the input type, independently of the read interface. + +New resolution propagates these constraints for type-argument inference and callback checking. +It does not perform complete structural subtyping checks; InvalML does not yet support record types. Under new resolution, recursive type aliases must be guarded: every recursive cycle must pass through a record, tuple, function arrow, or nominal type. diff --git a/hkmc2/jvm/src/test/scala/hkmc2/ErasureInputsTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/ErasureInputsTest.scala new file mode 100644 index 0000000000..5459cd6a7d --- /dev/null +++ b/hkmc2/jvm/src/test/scala/hkmc2/ErasureInputsTest.scala @@ -0,0 +1,33 @@ +package hkmc2 +package semantics + +import org.scalatest.funsuite.AnyFunSuite +import hkmc2.utils.* + +/** Diff tests exercise representations. This checks the separate ownership property: + * consumers and later worksheet blocks cannot mutate completed observation snapshots. + */ +class ErasureInputsTest extends AnyFunSuite: + test("completed observations are isolated from worksheet and import consumers"): + given owner: Elaborator.State = new Elaborator.State + given defining: ResolutionContext = owner.newResolverState.context + val inputs = owner.newResolverState.erasureInputs + val symbol = VarSymbol(new syntax.Tree.Ident("value")) + val source = new TermShapeHost + inputs.register(symbol)(source.subscribeToShapes) + val initial = DynShape() + source.publish(initial) + defining.completeBlock(Term.UnitVal()) + val snapshot = inputs.shapes(symbol) + val first = new Elaborator.State().newResolverState.context + val second = new Elaborator.State().newResolverState.context + val later = UnknownValueShape.at(Term.UnitVal()) + val other = UnknownValueShape.at(Term.Lit(syntax.Tree.IntLit(1))) + source.publish(later)(using first) + source.publish(other)(using second) + source.publish(other)(using defining) + assert(source.currentShapes(using first).toSet == Set(initial, later)) + assert(source.currentShapes(using second).toSet == Set(initial, other)) + assert(source.currentShapes(using defining).toSet == Set(initial, other)) + assert(snapshot == List(initial)) + assert(inputs.shapes(symbol) == snapshot) diff --git a/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala index 8b2c31962f..213b4b3192 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala @@ -214,8 +214,8 @@ class TypeRelationTest extends AnyFunSuite: value.typeInterpretation = S(element.resolution) val next = Term.UnitVal() next.typeInterpretation = S(resolution) - val valueField = RcdField.signature(Term.Lit(Tree.StrLit("value")), value) - val nextField = RcdField.signature(Term.Lit(Tree.StrLit("next")), next) + val valueField = RcdField.signature(Term.Lit(Tree.StrLit("value")), value, false, N) + val nextField = RcdField.signature(Term.Lit(Tree.StrLit("next")), next, false, N) val source: Term.Rcd = Term.Rcd(false, List(valueField, nextField)) resolution.publish(TypeShape.Record(source, List(valueField -> element.resolution, nextField -> resolution))) DeclaredType(resolution, Map.empty, TypeSubstitution.empty, true)(N) @@ -574,7 +574,7 @@ class TypeRelationTest extends AnyFunSuite: val left = new TypeResolution(Term.UnitVal(), _ => fail("Unexpected type error")) val right = new TypeResolution(Term.UnitVal(), _ => fail("Unexpected type error")) def recursive(resolution: TypeResolution): Unit = - val next = RcdField.signature(Term.Lit(Tree.StrLit("next")), Term.UnitVal()) + val next = RcdField.signature(Term.Lit(Tree.StrLit("next")), Term.UnitVal(), false, N) val source: Term.Rcd = Term.Rcd(false, List(next)) resolution.publish(TypeShape.Record(source, List(next -> resolution))) recursive(left) @@ -584,10 +584,10 @@ class TypeRelationTest extends AnyFunSuite: case (value: RecordTypeShape) :: Nil => value case _ => fail("Expected one merged record") val first = record(intersection) - val next = first.fields("next") + val next = first.fields("next").output val saturated = record(next) val counts = (left.inferenceHost.listeners.size, right.inferenceHost.listeners.size) (1 to 1000).foreach: _ => - assert(record(saturated.fields("next")) eq saturated) + assert(record(saturated.fields("next").output) eq saturated) assert((left.inferenceHost.listeners.size, right.inferenceHost.listeners.size) == counts) assert(h.state.allocatedTypeInstanceCount == 0) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala index 53152fadc5..acb61b4ebd 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala @@ -374,7 +374,8 @@ end Block object Block: /** The union of the types returned from `block`, whose canonical form is their least upper bound. * - * `N` means there are no returns; an untyped return contributes `Unknown` instead. Nested function, lambda, + * `N` means there are no constraining returns: `undefined` can inhabit any representation. Other untyped + * returns contribute `Unknown` instead. Nested function, lambda, * and class bodies have their own returns and are excluded. All other returns count, including unreachable * ones, since backends such as Wasm still validate their types. * @@ -385,7 +386,7 @@ object Block: val types = Buffer.empty[ErasedValueType] val collector = new BlockTraverserShallow: override def applyBlock(b: Block): Unit = b match - case Return(res) => types += res.erasedValueType_! + case Return(res) => if !res.isUndefined then types += res.erasedValueType_! case _ => super.applyBlock(b) collector.applyBlock(block) Option.when(types.nonEmpty)(ErasedType.Union.mk(types)) @@ -833,7 +834,13 @@ private[codegen] object InlinerBodySummary: object FunDefn: def withFreshSymbol(owner: Opt[InnerSymbol], sym: BlockMemberSymbol, params: Ls[LoweredParamList], body: Block)(configOverride: Opt[Config], annotations: Ls[Annot])(using State) = - val tSym = TermSymbol(syntax.Fun, owner, Tree.Ident(sym.nme), erasedType = N) + // * Generated callables have a known function representation even when their result is unknown. + // * In particular, lowering a lambda must not introduce a checked Function cast that makes + // * its uses opaque to dead-parameter elimination and deforestation. + // * Definitions without parameter lists are getters, whose value representation is still unknown. + val erasedType = Option.when(params.nonEmpty): + ErasedType.FuncRef(S(false), params.map(_.params.map(_.sym.erasedType)), N) + val tSym = TermSymbol(syntax.Fun, owner, Tree.Ident(sym.nme), erasedType) sym.tsym = S(tSym) FunDefn(owner, sym, tSym, params, body)(configOverride, annotations) @@ -1159,6 +1166,10 @@ sealed abstract class Result extends Located, HasErasedType: case Value.Lit(_: Tree.StrLit) => S(ErasedType.Str) case Value.Lit(_: Tree.BoolLit) => S(ErasedType.Bool) // * Note: `UnitLit` stays untyped: Neither `null` nor `undefined` can be reasonably typed as `Unit` + // * WORKAROUND (see `semantics.BuiltinOperatorResults`): builtin operators have no declared signature, + // * so their results are approximated from the representations of their operands. + case Call(Value.SimpleRef(op: BuiltinSymbol), args :: Nil) if args.forall(_.spread.isEmpty) => + ErasedType.builtinOperatorResult(op.nme, args.map(_.value)) case Call(fun, argss) => fun.targetSymbol match case S(ts: TermSymbol) => ts.erasedType match case S(ErasedType.FuncRef(rsc, paramLists, ret)) => @@ -1190,8 +1201,21 @@ sealed abstract class Result extends Located, HasErasedType: case sym => sym.asCls.flatMap(_.erasedType) // * A tuple literal is typed as `Array` at runtime. case Tuple(_, _) => S(ErasedType.Array) + // * Avoid a redundant cast around a function literal: in JavaScript that + // * would also prevent assignment from supplying the function's inferred name. + case _: Lambda => S(ErasedType.Function(S(false))) case _ => N + /** `undefined` is an unspecified value of any representation, including unboxed primitives. + * Casts retain the expected representation but do not constrain the underlying unspecified value. + * Reading back an `undefined` stored in a slot is undefined behavior (see Runtime Representations in + * `doc/reference.md`), so a backend may materialize it as any value of the slot's representation. + */ + def isUndefined: Bool = this match + case Value.Lit(Tree.UnitLit(false)) => true + case Cast(value, _, _) => value.isUndefined + case _ => false + /** Coerces this result to `expected`, yielding it unchanged when no coercion is required. * * Narrowing to an unrelated type is reported as an error, and this result is yielded unchanged. @@ -1201,26 +1225,39 @@ sealed abstract class Result extends Located, HasErasedType: * [[Config]] of their own. */ def coerceTo(expected: ErasedType, loc: Opt[Loc])(using Ctx, State, Raise, Config): this.type | Cast = + coerceToOr[this.type | Cast](expected, loc)(identity): (actual, declared) => + raise: + ErrorReport(msg"Cannot use a value of type '${actual.describe}' at an unrelated type '${declared.describe}'" -> loc :: Nil, + source = Diagnostic.Source.Compilation) + this + + // `coerceTo` continuing with `coerced`, except that a value of a type unrelated to `expected` goes to `unrelated` + def coerceToOr[R](expected: ErasedType, loc: Opt[Loc])(coerced: (this.type | Cast) => R) + (unrelated: (CanonicalErasedType, CanonicalErasedType) => R)(using Ctx, State, Raise, Config): R = val actual = erasedValueType_!.canonicalize val declared = expected.canonicalize - ErasedType.needsCast(actual, declared) match - case S(false) => this + val cast = + if isUndefined && !declared.isInstanceOf[ErasedType.Incompatible] then S(true) + else ErasedType.needsCast(actual, declared) + cast match + case S(false) => coerced(this) case S(true) => val target = expected match case ft: ErasedFuncType => ErasedType.Function(ft.rsc) case v: ErasedValueType => v - Cast(this, target, config.checkCasts)(loc) + coerced(Cast(this, target, config.checkCasts && !isUndefined)(loc)) case N => // * An `Incompatible` side is not an unrelated type but an unrepresentable one, so it gets its own message. def membersOf(et: CanonicalErasedType): Opt[(CanonicalErasedValueType, CanonicalErasedValueType)] = et match case ErasedType.Incompatible(l, r) => S(l -> r) case _ => N - val message = membersOf(actual).orElse(membersOf(declared)) match - case S((l, r)) => msg"Types '${l.describe}' and '${r.describe}' have no common representation" - case N => msg"Cannot use a value of type '${actual.describe}' at an unrelated type '${declared.describe}'" - raise: - ErrorReport(message -> loc :: Nil, source = Diagnostic.Source.Compilation) - this + membersOf(actual).orElse(membersOf(declared)) match + case S((l, r)) => + raise: + ErrorReport(msg"Types '${l.describe}' and '${r.describe}' have no common representation" -> loc :: Nil, + source = Diagnostic.Source.Compilation) + coerced(this) + case N => unrelated(actual, declared) /* mayRaiseEffects indicates whether this call may raise effect (algebraic effect), * regardless of whether the check for effect is inserted or not. @@ -1335,7 +1372,8 @@ case class Instantiate(mut: Bool, cls: Path, argss: Ls[Ls[Arg]])(val metadata: I * * Invariants: * - `value` is not a `Cast`. - * - `target` must be a proper subtype of `value`'s erased type. + * - `target` must be a proper subtype of `value`'s erased type, unless `value` is `undefined`, whose + * representation is supplied by the cast. */ case class Cast private(value: Result, target: ErasedValueType, check: Bool)(val toLoc: Opt[Loc]) extends Path: def withLoc(loc: Opt[Loc]): Cast = if loc == toLoc then this else copy()(loc) @@ -1343,7 +1381,8 @@ case class Cast private(value: Result, target: ErasedValueType, check: Bool)(val object Cast: /** Builds a cast while collapsing a nested cast. * - * This node is malformed if the target type is not a proper subtype of the value's erased type. + * Except for `undefined`, this node is malformed if the target type is not a proper subtype of the value's + * erased type. * * Collapsing `Cast(Cast(v, T), U)` to `Cast(v, U)` drops the inner test. The `T`-typed intermediate goes with it, * so no value is left in a wrongly-typed slot. However, an unchecked failure *can be lost* - if a cast is diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala index 8b8944ffcf..38026f4937 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala @@ -1145,6 +1145,12 @@ class BlockSimplifier override def applyPath(v: Path)(k: Path => Block): Block = v match + case c: Cast if !inDryRun => + applyResult(c): + case p: Path => k(p) + case r => + val tmp = TempSymbol(N, erasedType = r.erasedValueType, "cast") + Scoped(Set.single(tmp), Assign(tmp, r, k(tmp.asPath))) case Value.SimpleRef(loc: LocalVar) if !inDryRun && !capturedVars(loc) => val rs = accessAssignedResults(loc) @@ -1183,6 +1189,16 @@ class BlockSimplifier // Some partial evaluation – TODO: move to IR smart constructors r match + // * Propagation and inlining can expose an operand already below the cast target. + // * Recheck after rewriting the operand, including checked casts: the subtype proof + // * makes that check redundant. Keep every narrowing cast and its evaluation order. + case c @ Cast(value, target, check) if !inDryRun => + applyResult(value): value2 => + if ErasedType.needsCast(value2.erasedValueType_!.canonicalize, target.canonicalize).contains(false) then + registerChange(s"rm redundant cast ${c.showDbg}") + k(value2) + else k(if value2 is value then c else Cast(value2, target, check)(c.toLoc)) + // * Try to propagate pure calls case Value.SimpleRef(loc: LocalVar) if !inDryRun && !capturedVars(loc) => assignedPureCallPrefix(loc) match @@ -1192,6 +1208,30 @@ class BlockSimplifier case N => super.applyResult(r)(k) + // Evaluate checked/effectful arguments at the original call site, leaving the + // partial application pure so later applications can combine with it. Snapshot + // earlier arguments too: a later argument may change the locals they read. + // A direct member reference needs no such snapshot of the function itself. + case c @ Call(fun: Value.MemberRef, argss) if !inDryRun && c.isKnownUnsaturatedCall && + argss.forall(_.forall(_.spread.isEmpty)) && !c.isPure => + val lastImpure = argss.flatten.lastIndexWhere(a => !a.value.isPure) + def rewriteLists(rest: Ls[Ls[Arg]], index: Int)(k: Ls[Ls[Arg]] => Block): Block = rest match + case Nil => k(Nil) + case args :: tail => + def rewriteArgs(rest: Ls[Arg], index: Int)(k: Ls[Arg] => Block): Block = rest match + case Nil => k(Nil) + case arg :: tail => applyArg(arg): arg2 => + if index <= lastImpure then + val tmp = TempSymbol(N, erasedType = arg2.value.erasedValueType, "arg") + Scoped(Set.single(tmp), Assign(tmp, arg2.value, + rewriteArgs(tail, index + 1)(args => k(arg2.copy(value = tmp.asPath) :: args)))) + else rewriteArgs(tail, index + 1)(args => k(arg2 :: args)) + rewriteArgs(args, index): args2 => + rewriteLists(tail, index + args.size)(tail2 => k(args2 :: tail2)) + registerChange(s"hoist partial-call arguments ${c.showDbg}") + rewriteLists(argss, 0): argss2 => + k(Call(fun, argss2.ne_!)(c.metadata, c.toLoc)) + // * Try to combine pure calls (typically unsaturated calls) assigned to a variable into the current call case c @ Call(Value.SimpleRef(loc: LocalVar), argss) if !inDryRun && !capturedVars(loc) => assignedPureCallPrefix(loc) match diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala index 6982c55934..96a8841684 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadConstructorElim.scala @@ -27,6 +27,88 @@ object NoDeadConstructorElim extends DeadConstructorElimResult: val deadCtors = Set.empty[ConcreteCtorId] +/** Prove that skipping construction omits only initialization of the discarded instance and + * its local slots. This reads the current IR definition, including superclass construction; + * missing definitions, cycles, calls, checks, and control flow are not assumed harmless. + * Callers separately preserve argument evaluation and any qualified constructor lookup. + */ +private[codegen] object ConstructorElision: + def isHarmless(ctor: CtorCls)(using Elaborator.State): Bool = ctor match + case _: Int => true + // A singleton producer refers to an already initialized object/module. + case _: ModuleOrObjectSymbol => true + case cls: ClassSymbol => cls.irClsLikeDefn.exists(harmlessClass(_, Set.empty)) + + /** Field projection additionally relies on the flow analysis's positional mapping from source + * fields to constructor arguments. Validate that mapping against the current IR parameter list + * and stores; harmless updates to an instance can still change the value of a projected field. + */ + def preservesParameterFields(ctor: Ctor)(using Elaborator.State): Bool = ctor.ctor match + case cls: ClassSymbol => cls.irClsLikeDefn.exists: definition => + val params = (definition.paramsOpt.toList ::: definition.auxParams).flatMap(_.allParams).map(_.sym) + val expected = ctor.args.zip(params).collect { case ((field: TermSymbol, _), param) => field -> param }.toMap + if params.length != ctor.args.length || expected.size != ctor.args.length then false + else + def isParameter(value: Result, param: VarSymbol): Bool = value match + case Value.SimpleRef(sym) => sym is param + case Cast(value, _, false) => isParameter(value, param) + case _ => false + def stores(block: Block, inBody: Bool): Opt[Set[TermSymbol]] = block match + case assign @ AssignField(Value.This(self), _, value, rest) if inBody && (self is definition.isym) => + for + field <- assign.symbol.flatMap(_.asTrm) + param <- expected.get(field) + if isParameter(value, param) + restFields <- stores(rest, inBody) + yield restFields + field + case Define(value: ValDefn, rest) if inBody => + for + param <- expected.get(value.tsym) + if isParameter(value.rhs, param) + restFields <- stores(rest, inBody) + yield restFields + value.tsym + case Assign(sym: VarSymbol, _, _) if params.contains(sym) => N + case Assign(NoSymbol, Call(Value.SimpleRef(parentCall), _), rest) + if parentCall is Elaborator.State.superSymbol => + if inBody then N else stores(rest, inBody) + case Assign(_, _: Path, rest) => stores(rest, inBody) + case Scoped(_, body) => stores(body, inBody) + case Begin(first, rest) => + for + firstFields <- stores(first, inBody) + restFields <- stores(rest, inBody) + yield firstFields ++ restFields + case End(_) => S(Set.empty) + case _ => N + stores(definition.preCtor, false).isDefined && stores(definition.ctor, true).exists(expected.keySet.subsetOf) + case _ => true + + private def harmlessClass(cls: ClsLikeDefn, visiting: Set[Symbol])(using Elaborator.State): Bool = + if visiting(cls.isym) then false + else + val next = visiting + cls.isym + val params = (cls.paramsOpt.toList ::: cls.auxParams).flatMap(_.allParams).map(_.sym).toSet[Symbol] + def harmlessBlock(block: Block, locals: Set[Symbol]): Bool = block match + case AssignField(Value.This(self), _, value: Path, rest) if (self is cls.isym) && value.isPure => + harmlessBlock(rest, locals) + case Define(value: ValDefn, rest) if value.owner.contains(cls.isym) && value.rhs.isPure => + harmlessBlock(rest, locals) + case Assign(local, value: Path, rest) if value.isPure && (local match + case sym: LocalVarSymbol => locals(sym) + case NoSymbol => true) => + harmlessBlock(rest, locals) + case Assign(NoSymbol, Call(Value.SimpleRef(parentCall), argss), rest) + if (parentCall is Elaborator.State.superSymbol) && argss.forall(_.forall(arg => arg.spread.isEmpty && arg.value.isPure)) => + cls.parentPath.flatMap(_.targetSymbol).collect { case parent: ClassSymbol => parent } + .flatMap(_.irClsLikeDefn).exists(harmlessClass(_, next)) && harmlessBlock(rest, locals) + case Scoped(symbols, body) => harmlessBlock(body, locals ++ symbols) + case Begin(first, rest) => harmlessBlock(first, locals) && harmlessBlock(rest, locals) + case End(_) => true + case _ => false + harmlessBlock(cls.preCtor, params) && harmlessBlock(cls.ctor, params) + end harmlessClass + + class DeadConstructorElimSolver(val constraintSolver: FlowConstraintSolver, traceLogger: TraceLogger) extends DeadConstructorElimResult: given tl: TraceLogger = traceLogger given fState: FlowAnalysis.State = constraintSolver.fState @@ -38,22 +120,7 @@ class DeadConstructorElimSolver(val constraintSolver: FlowConstraintSolver, trac def isRemovable(ctorSite: Result): Bool = ctorSite match case _: Tuple => true - case CtorProducer(cls: ClassSymbol, args, selectedFrom) => - // TODO: should be able to remove this later when we can reason about - // ctor flow and side effect properly - def hasSimpleCtor(cls: ClsLikeDefn): Bool = - val params = (cls.paramsOpt.toList ::: cls.auxParams).flatMap(_.allParams).map(_.sym).toSet[Symbol] - @annotation.tailrec - def onlyStoresParams(b: Block): Bool = b match - case AssignField(Value.This(self), _, Value.SimpleRef(p), rest) if self is cls.isym => - onlyStoresParams(rest) - case Define(ValDefn(_, _, Value.SimpleRef(p)), rest) => - onlyStoresParams(rest) - case End(_) => true - case _ => false - onlyStoresParams(cls.preCtor) && onlyStoresParams(cls.ctor) - end hasSimpleCtor - cls.irClsLikeDefn.exists(hasSimpleCtor) + case CtorProducer(cls: ClassSymbol, _, _) => ConstructorElision.isHarmless(cls) case _ => false val allCtorStrats = collector.allRealCtors.filter(_.instantiationId.isDefined) @@ -89,6 +156,7 @@ object DeadConstructorElim: tl: TL, raise: Raise, eState: Elaborator.State, + eCtx: Elaborator.Ctx, symbolPrinter: SymbolPrinter, ): Program = cfg.flowBasedOpt.fold(p): FlowAnalysisBasedRewrite.rewriteWith(p, _, eta = false, dpe = false) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala index 3c9e3d2b08..d2346fef39 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/DeadParamElim.scala @@ -90,6 +90,7 @@ class DeadParamElimSolver(val constraintSolver: FlowConstraintSolver, traceLogge case c: ConsFun => (c, idx), (consFun, idx) => consFun.srcs.collect: case p: ProdFun => (p, idx), + _ => Nil, prodRoots, consRoots, ) @@ -133,6 +134,7 @@ object DeadParamElim: tl: TL, raise: Raise, eState: Elaborator.State, + eCtx: Elaborator.Ctx, symbolPrinter: SymbolPrinter, ): Program = cfg.flowBasedOpt.fold(p): FlowAnalysisBasedRewrite.rewriteWith(p, _, eta = false, dce = false) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/ErasedType.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/ErasedType.scala index e2dc745cb7..e05065325b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/ErasedType.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/ErasedType.scala @@ -150,6 +150,38 @@ object ErasedType: */ def Function(rsc: Opt[Bool]): ErasedValueType = ErasedType.ValueLike(rsc, ctx.builtins.Function) + /** WORKAROUND (see [[semantics.BuiltinOperatorResults]]): the representation of an application of the builtin + * operator named `op`, approximated from the representations of its operands with the same rules as resolution. + * Comparing representations requires a context, so the choice is deferred until the type is canonicalized. + * Only the JavaScript backend sees these applications: lowering for Wasm replaces them by intrinsic calls. + */ + def builtinOperatorResult(op: Str, operands: Ls[HasErasedType]): Opt[ErasedValueType] = + import semantics.BuiltinOperatorResults, BuiltinOperatorResults.{PrimitiveClass, Alternative} + if !BuiltinOperatorResults.approximates(op, operands.length) then N + else S(ErasedType.ValueLike(rsc = S(false), { + val builtins = ctx.builtins + def primitiveOf(operand: HasErasedType): Opt[PrimitiveClass] = + val actual = operand.erasedValueType_!.canonicalize + def within(cls: ClassSymbol) = + needsCast(actual, ErasedType.ValueLike(rsc = S(false), cls).canonicalize).contains(false) + if within(builtins.Int) then S(PrimitiveClass.Int) + else if within(builtins.Num) then S(PrimitiveClass.Num) + else if within(builtins.Str) then S(PrimitiveClass.Str) + else if within(builtins.Bool) then S(PrimitiveClass.Bool) + else if within(builtins.BigInt) then S(PrimitiveClass.BigInt) + else N + BuiltinOperatorResults(op, operands.map(primitiveOf)) match + case S(Alternative.Of(primitive) :: Nil) => primitive match + case PrimitiveClass.Int => builtins.Int + case PrimitiveClass.Num => builtins.Num + case PrimitiveClass.Str => builtins.Str + case PrimitiveClass.Bool => builtins.Bool + case PrimitiveClass.BigInt => builtins.BigInt + // Applications that yield an operand (the comma and short-circuiting operators) do not reach the IR as + // calls. Like applications without an approximation, they keep the top representation. + case _ => builtins.Anything + })) + /** Determines the direct parent from published metadata, treating a missing header as unknown. */ private def parentOf(sym: BaseTypeSymbol): ClassHeader.Parent = sym.irClassHeader.fold(ClassHeader.Parent.Unknown)(_.parent) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Erasure.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Erasure.scala index 10bf437948..6f75172270 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Erasure.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Erasure.scala @@ -11,14 +11,81 @@ import hkmc2.Message.MessageContext /** The capability to inspect completed resolution. Created only after elaboration/resolution; * executable lowering additionally requires this instance's whole-program pass to have finished. + * + * The representations this pass infers from resolution's observations are as binding as declared ones: + * lowering reports a value whose representation is unrelated to its slot's, or a slot whose values have + * no common representation, as a compilation error (see Runtime Representations in `doc/reference.md`). */ final class Erasure private (using Config, Ctx, State)(using Raise): private given Erasure = this private var completed: Opt[Statement] = N + private val valueTypes = mutable.Map.empty[TermShape, Opt[ErasedValueType]] + private val bodyResults = mutable.Map.empty[TermSymbol, Opt[ErasedValueType]] def requireCompleted(statement: Statement): Unit = assert(completed.exists(_ is statement), "Lowering requires completed erasure of this program") + /** Async definitions expose a promise and generators an iterator, but their original + * bodies produce the payload (of the promise, or of the generator's final step). + * Lowering must coerce explicit and implicit returns before they are wrapped. + */ + def bodyResultType(td: TermDefinition): Opt[ErasedValueType] = + bodyResults.getOrElse(td.tsym, td.tsym.declaredResultType) + + private def valueType(shape: TermShape): Opt[ErasedValueType] = valueTypes.getOrElseUpdate(shape, shape match + case MarkedShape(value, _) => valueType(value) + case ContextualShape(value, _) => valueType(value) + case SpecializedShape(value, _, _) => valueType(value) + case nominal: NominalInstanceView => S(nominalType(nominal.defn)) + case base: BaseShape => S(nominalType(base.defn)) + case construction: NewShape => S( + if construction.isSaturated then ErasedType.ValueLike(S(false), construction.cls) + else ErasedType.Function(S(false))) + case application: AppShape => S( + if !application.isSaturated then ErasedType.Function(S(false)) + else application.applicationHead._1 match + case definition: DefnShape => definition.clsDef.fold[ErasedValueType](ErasedType.Unknown)(nominalType) + case _ => ErasedType.Unknown) + case definition: DefnShape => S(definition.defn match + case module: ModuleOrObjectDef => nominalType(module) + case _: TermDefinition => ErasedType.Function(S(false)) + // A class value denotes the constructor object, not an instance of that class. + case _ => ErasedType.Unknown) + case intro: IntroShape => intro.trm match + case _: Lam => S(ErasedType.Function(S(false))) + case UnitVal() => S(ErasedType.Unit) + case Lit(_: syntax.Tree.IntLit) => S(ErasedType.Int) + case Lit(_: syntax.Tree.DecLit) => S(ErasedType.Num) + case Lit(_: syntax.Tree.StrLit) => S(ErasedType.Str) + case Lit(_: syntax.Tree.BoolLit) => S(ErasedType.Bool) + // Undefined places no constraint on the slot's representation. An undefined-only + // slot still receives the ordinary default representation and keeps it across consumers. + case Lit(syntax.Tree.UnitLit(false)) => N + case Lit(syntax.Tree.UnitLit(true)) => S(ErasedType.Unknown) + case _: CallableTypeShape => S(ErasedType.Function(S(false))) + case _: TupleShape => S(ErasedType.Array) + // An intersection describes one value through several interfaces, not alternative values. + // Keep the same conservative representation as an intersection in a declared signature. + case _: IntersectionView => S(ErasedType.Unknown) + case _: (RecordShape | RecordTypeShape | DynShape | UnknownValueShape | OpaqueTypeShape | RigidTypeShape | ErrShape) => + S(ErasedType.Unknown) + case _: InstanceShape => + softAssert(false, "Erasure inputs must interpret contextual instance types before completion") + S(ErasedType.Unknown) + ) + + private def nominalType(definition: ClassLikeDef): ErasedValueType = definition.sym match + case symbol: (ClassSymbol | ModuleOrObjectSymbol) => ErasedType.ValueLike(S(false), symbol) + case _ => ErasedType.Unknown + + private def inferredType(symbol: Symbol): Opt[ErasedValueType] = + if !config.language.useNewResolution then N + else summon[State].newResolverState.erasureInputs.shapes(symbol).flatMap(valueType) match + case Nil => N + // Unknown is a real candidate, not the identity of this join: in particular, + // combining it with an unboxed primitive must preserve incompatibility. + case types => S(ErasedType.Union.mk(types)) + /** Erase an interpretation registered during elaboration, without installing new listeners. */ private def erase(resolution: TypeResolution, seen: Set[TypeResolution]): ErasedValueType = import TypeShape.* @@ -67,6 +134,25 @@ final class Erasure private (using Config, Ctx, State)(using Raise): case N => (ps :: Nil, rhs) case _ => N + /** An override without a result annotation implements the result of the member it overrides as if it declared it: + * lowering coerces the values of its body to the inherited representation, with checked casts where resolution + * observes wider values (as when an override computes its result with dynamically typed operations, or when its + * body never returns normally). A representation within the inherited one is kept, being more precise. An unrelated + * one is kept as well, for `checkOverride` to report: such values cannot be coerced to the inherited result. + */ + private def overrideResultType(td: TermDefinition, observed: Opt[ErasedValueType]): Opt[ErasedValueType] = + td.tsym.overriddenMembers.headOption.filter(_.isErased) match + case N => observed + case S(base) => + val inherited = base.declaredResultType + observed match + case N => inherited + case S(tpe) => + ErasedType.needsCast(tpe.canonicalize, inherited.getOrElse(ErasedType.Unknown).canonicalize) match + case S(false) => observed + case S(true) => inherited + case N => observed + private def definitionType(td: TermDefinition): Opt[ErasedType] = val declared = Annot.declareModifierOf(td.annotations).isDefined // A declared function with no written parameters obtains its calling convention from its arrows. @@ -89,7 +175,21 @@ final class Erasure private (using Config, Ctx, State)(using Raise): if config.language.useNewResolution && td.sign.nonEmpty && (td.k is syntax.Fun) && !td.flags.hasResultAnnotation && td.params.nonEmpty && !td.modulefulness.isModuleful then S(fullResult(td.sign.get.typeInterpretation.get, td.params.length, Set.empty)) - else declaredType(resultSign, td.modulefulness) + else declaredType(resultSign, td.modulefulness).orElse: + val observed = inferredType(td.tsym) + // An async/generator override's inherited callable result describes its wrapper, + // not the payload returned by this body. Check that wrapper in `checkOverride` below. + if config.language.useNewResolution && td.resultWrapperAnnotation.nonEmpty then observed + else overrideResultType(td, observed) + bodyResults(td.tsym) = resultType + // The body produces the payload; callers receive the wrapper object instead. + // Resolution gives calls of such functions the same wrapper shapes. + val callableResult = + if !config.language.useNewResolution then resultType + else td.resultWrapperAnnotation match + case S(_: Annot.Async) => S(ErasedType.ValueLike(S(false), summon[Ctx].builtins.Promise)) + case S(_: Annot.Generator) => S(ErasedType.ValueLike(S(false), summon[Ctx].builtins.Iterator)) + case N => resultType td.k match case syntax.Fun => val paramLists = sigShape match @@ -99,15 +199,16 @@ final class Erasure private (using Config, Ctx, State)(using Raise): // Member getters and global declarations denote their result directly. val physicalParamLists = if paramLists.isEmpty && td.owner.isEmpty && !declared then Nil :: Nil else paramLists - if physicalParamLists.isEmpty then resultType - else S(ErasedType.FuncRef(rsc = S(false), physicalParamLists, resultType)) + if physicalParamLists.isEmpty then callableResult + else S(ErasedType.FuncRef(rsc = S(false), physicalParamLists, callableResult)) case _: syntax.Val => resultType + case syntax.RecordField => if config.language.useNewResolution then resultType else N case _ => N private def run(root: Statement): Unit = - val visited = mutable.Set.empty[Identity[Statement]] - val statements = mutable.ArrayBuffer.empty[Statement] - val parameters = mutable.ArrayBuffer.empty[Param] + val declarations = new ErasureDeclarations(root) + val statements = declarations.statements + val parameters = declarations.parameters.values val parentConstructions = mutable.Set.empty[Identity[Statement]] def parentHead(term: Term): Unit = if parentConstructions.add(Identity(term)) then term match // Legacy resolution can expand a parent reference to an implicit constructor call. @@ -117,49 +218,9 @@ final class Erasure private (using Config, Ctx, State)(using Raise): case App(lhs, _) => parentHead(lhs) case TyApp(lhs, _) => parentHead(lhs) case _ => () - def params(ps: ParamList): Unit = ps.foreach(parameters += _) - // Handler continuations and rejected class rest parameters can survive as references after their - // parameter list has been removed. The symbol retains the source declaration needed for erasure. - def referencedParameter(sym: VarSymbol): Unit = sym.decl match - case S(p: Param) => - parameters += p - p.subTerms.foreach(visit) + statements.foreach: + case cls: ClassLikeDef => cls.ext.foreach(parentHead) case _ => () - def visit(statement: Statement): Unit = if visited.add(Identity(statement)) then - statements += statement - statement match - case td: TermDefinition => td.params.foreach(params); td.tparams.foreach(_.foreach(parameters += _)) - case cls: ClassLikeDef => - cls.paramsOpt.foreach(params) - cls.auxParams.foreach: ps => - params(ps) - ps.subTerms.foreach(visit) - cls.ext.foreach: parent => - parentHead(parent) - visit(parent) - cls match - case pat: PatternDef => - parameters ++= pat.parameters - pat.parameters.flatMap(_.subTerms).foreach(visit) - pat.pattern.subTerms.foreach(visit) - case _ => () - case Lam(ps, _) => params(ps) - case Handle(_, _, _, _, definitions, _) => definitions.foreach: d => - referencedParameter(d.resumeSym) - visit(d.td) - case Ref(sym: VarSymbol) => referencedParameter(sym) - case SimpleRef(sym: VarSymbol) => referencedParameter(sym) - case New(_, _, refinement) => refinement.foreach: (symbol, body) => - symbol.defn.fold(visit(body.blk))(visit) - case Rcd(_, stats) => stats.foreach(visit) - case _ => () - statement.subStatements.foreach(visit) - // Legacy resolution may insert executable terms (for example implicit applications). - // Expanded nodes may share their children with the source tree; visit their identities only once. - statement match - case term: Resolvable => visit(term.expanded) - case _ => () - visit(root) def parentClasses(parent: New): Ls[ClassLikeDef] = (parent.cls.resolvedSym.flatMap(_.asClsOrMod).flatMap(_.defn).toList ::: @@ -250,8 +311,29 @@ final class Erasure private (using Config, Ctx, State)(using Raise): (direct ::: app.shapes.toList.flatMap(constructed)).distinct.foreach(checkConstruction(app, _)) case _ => () + // An override shares the calling convention of the member it overrides: calls of that member reach it, and the + // Wasm backend compiles it with that member's physical signature. Its parameters without declared types therefore + // take the representations of the inherited parameters (even unknown ones), rather than of their own observed + // inputs; `checkOverride` checks the declared ones. + val inheritedParams: Map[VarSymbol, Param] = statements.iterator.flatMap: + case td: TermDefinition => td.tsym.overriddenMembers.headOption.flatMap(_.defn).iterator.flatMap: base => + val leading = + if td.params.map(_.params.length) != base.params.map(_.params.length) then Nil + else td.params.zip(base.params).flatMap((ps, bps) => ps.params.zip(bps.params)) + (leading ::: correspondingVariadicParameters(td, base)).iterator.map((param, inherited) => param.sym -> inherited) + case _ => Iterator.empty + .toMap + def eraseParameter(p: Param, visiting: Set[VarSymbol]): Unit = if !p.sym.isErased then + p.sym.erasedType = declaredType(p.sign, p.modulefulness).orElse: + inheritedParams.get(p.sym) match + // Parameters of members defined in earlier programs are already erased. + case S(inherited) if declarations.parameters.contains(inherited.sym) && !visiting(p.sym) => + eraseParameter(inherited, visiting + p.sym) + inherited.sym.erasedType + case S(inherited) => Option.when(inherited.sym.isErased)(inherited.sym.erasedType).flatten + case N => inferredType(p.sym) parameters.foreach: p => - if !p.sym.isErased then p.sym.erasedType = declaredType(p.sign, p.modulefulness) + eraseParameter(p, Set.empty) // Public fields have a BlockMemberSymbol; private/auxiliary constructor fields have a TermSymbol. // Their source signatures belong to the parameter, not to the synthesized field definition. val field = p.fldSym.flatMap: @@ -259,15 +341,153 @@ final class Erasure private (using Config, Ctx, State)(using Raise): case sym: BlockMemberSymbol => sym.tsym case _ => N field.foreach: sym => - if !sym.isErased then sym.erasedType = p.sym.erasedType + if !sym.isErased then sym.erasedType = + if p.flags.mut then declaredType(p.sign, p.modulefulness).orElse(inferredType(sym)) + else p.sym.erasedType + val observed = + if config.language.useNewResolution then summon[State].newResolverState.erasureInputs.symbols + else Nil + // Constructor companions have no source body. Their physical argument lists + // are the class's lists, and a saturated call always produces its instance. statements.foreach: - case td: TermDefinition if !td.tsym.isErased => td.tsym.erasedType = definitionType(td) + case cls: ClassLikeDef => cls.ctorSym.foreach: symbol => + if !symbol.isErased then symbol.erasedType = + if config.language.useNewResolution then S(ErasedType.FuncRef(S(false), + (cls.paramsOpt.toList ::: cls.auxParams).map(_.params.map(_.sym.erasedType)), S(nominalType(cls)))) + else N + case _ => () + val definitions = statements.collect { case td: TermDefinition => td } ++ observed.collect { + case sym: TermSymbol => sym.defn }.flatten + // A handler's compiled declaration omits its resumption parameter list; + // retain the traversed declaration instead of recovering the original from its symbol. + // An override's result can be the inherited one (see `overrideResultType`), so the members a definition + // overrides are erased first. Overriding edges are acyclic, but erroneous inheritance cycles must not diverge. + val definitionsBySymbol = definitions.distinctBy(_.tsym).map(td => td.tsym -> td).toMap + def eraseDefinition(td: TermDefinition, visiting: Set[TermSymbol]): Unit = + if !td.tsym.isErased && !visiting(td.tsym) then + td.tsym.overriddenMembers.flatMap(definitionsBySymbol.get).foreach(eraseDefinition(_, visiting + td.tsym)) + td.tsym.erasedType = definitionType(td) + definitions.distinctBy(_.tsym).foreach(eraseDefinition(_, Set.empty)) + declarations.variables.foreach: + case sym: VarSymbol if !sym.isErased => sym.erasedType = inferredType(sym) + case sym: TermSymbol if !sym.isErased => sym.erasedType = inferredType(sym) + // A temporary's erased type is populated at most once (see TempSymbol); without inferred inputs, it stays + // unknown, as it was before erasure. + case sym: TempSymbol if sym.erasedType.isEmpty => inferredType(sym).foreach(sym.populateErasedType(_)) case _ => () + // These checks compare erased signatures, so they run once every definition of the program is erased. statements.foreach: - case LetDecl(sym: TermSymbol, _) if !sym.isErased => sym.erasedType = N + case td: TermDefinition => + checkOverride(td) + checkExposedOpenMember(td, declarations) + checkExposedMutableField(td) case _ => () completed = S(root) + /** The rest tuple and trailing slots receive the same argument positions when both declarations + * have the same layout. Equal leading counts alone do not establish that correspondence: an + * optional prefix or a different trailing count changes which arguments enter the rest tuple. + */ + private def correspondingVariadicParameters(td: TermDefinition, base: TermDefinition): Ls[(Param, Param)] = + def layout(ps: ParamList) = + (ps.flags, ps.params.length, ps.leadingRequiredCount, ps.variadic.isDefined, ps.trailingParams.length) + if td.params.map(layout) != base.params.map(layout) then Nil + else td.params.zip(base.params).flatMap: (ps, bps) => + ps.variadic.toList.flatMap(_.allParams).zip(bps.variadic.toList.flatMap(_.allParams)) + + /** An override is reached through calls of the member it overrides, so it must accept every representation of the + * inherited parameters and produce only representations of the inherited result. The Wasm backend also compiles all + * the overrides of a method with the physical signature of the method introducing it, so on that target, an + * override called directly cannot accept representations that this signature cannot hold either: its parameters + * must have the representations of the inherited ones. Unchecked, such mismatches would be bridged by casts that + * fail at run time. Parameters without declared types already take the inherited representations (see + * `inheritedParams` in `run`), so only declared ones can fail the parameter checks. + */ + private def checkOverride(td: TermDefinition): Unit = + def within(actual: Opt[ErasedValueType], expected: Opt[ErasedValueType]): Bool = + ErasedType.needsCast(actual.getOrElse(ErasedType.Unknown).canonicalize, + expected.getOrElse(ErasedType.Unknown).canonicalize).contains(false) + def describe(tpe: Opt[ErasedValueType]): Str = tpe.getOrElse(ErasedType.Unknown).canonicalize.describe + def signature(sym: TermSymbol): (Ls[Ls[Opt[ErasedValueType]]], Opt[ErasedValueType]) = sym.erasedType match + case S(ft: ErasedFuncType) => (ft.paramLists, ft.ret) + case S(vt: ErasedValueType) => (Nil, S(vt)) + case N => (Nil, N) + if td.tsym.isErased then + val (params, result) = signature(td.tsym) + td.tsym.overriddenMembers.filter(_.isErased).foreach: base => + val (baseParams, baseResult) = signature(base) + def report(message: Message, loc: Opt[Loc]): Unit = raise(ErrorReport( + (message -> loc) :: (msg"The overridden member '${base.nme}' is defined here" -> base.toLoc) :: Nil, + source = Diagnostic.Source.Compilation)) + if !within(result, baseResult) then report( + msg"Overriding member '${td.tsym.nme}' produces values represented as '${describe(result) + }', which the member it overrides cannot produce: its results are represented as '${describe(baseResult)}'", + td.tsym.toLoc) + def checkParameter(param: Opt[ErasedValueType], baseParam: Opt[ErasedValueType], p: Param): Unit = + if !within(baseParam, param) then report( + msg"Parameter '${p.sym.nme}' of overriding member '${td.tsym.nme}' is represented as '${describe(param) + }', which cannot hold the values represented as '${describe(baseParam)}' that the member it overrides accepts", + p.toLoc) + else if (config.target is CompilationTarget.Wasm) && !within(param, baseParam) then report( + msg"Parameter '${p.sym.nme}' of overriding member '${td.tsym.nme}' receives values represented as '${ + describe(param)}', but Wasm calls it through the signature of the member it overrides, whose parameter is represented as '${ + describe(baseParam)}'", + p.toLoc) + // Parameter lists of different lengths are reported by the backends that require equal lengths. + if params.map(_.length) == baseParams.map(_.length) then + params.flatten.lazyZip(baseParams.flatten).lazyZip(td.params.flatMap(_.params)).foreach: (param, baseParam, p) => + checkParameter(param, baseParam, p) + base.defn.foreach: inherited => + correspondingVariadicParameters(td, inherited).foreach: (param, baseParam) => + checkParameter(param.sym.erasedType, baseParam.sym.erasedType, param) + + /** Subclasses in other compilation units implement an open member with the representations of its erased signature, + * which therefore must not depend on what this compilation unit happens to pass to the member or return from it. + * Interface exposure feeds values of unknown shape to the unannotated parameters of members reachable from the + * unit's interface (see InterfaceExposure); an open member receiving them must annotate its parameters and result. + * Exposure is not observed for a member whose parameters are all annotated, or which has none: overrides in other + * compilation units are then still checked against its erased signature by `checkOverride`. + */ + private def checkExposedOpenMember(td: TermDefinition, declarations: ErasureDeclarations): Unit = + if config.language.useNewResolution && (td.k is syntax.Fun) && Inheritance.isOpen(td) && !Inheritance.isExternal(td) then + val params = td.params.flatMap(_.allParams) + def exposed(p: Param): Bool = declarations.parameters.contains(p.sym) && + summon[State].newResolverState.erasureInputs.shapes(p.sym).exists: shape => + shape.applicationHead._1 match + case unknown: UnknownValueShape => unknown.fromPublicInterface + case _ => false + if params.exists(exposed) then + val missing = params.filter(_.sign.isEmpty).map(p => msg"Parameter '${p.sym.nme}' has no type annotation" -> p.toLoc) + ::: Option.when(td.resultSignature.isEmpty)(msg"Its result has no type annotation" -> td.tsym.toLoc).toList + if missing.nonEmpty then raise(ErrorReport( + (msg"Open member '${td.tsym.nme}' is exposed to other compilation units, which can override it; annotate the types of its parameters and result" -> td.tsym.toLoc) + :: missing, + source = Diagnostic.Source.Compilation)) + + /** Other compilation units assign values to a mutable field with the representation of its erased type, which + * therefore must not depend on the values that this compilation unit happens to assign to the field. Interface + * exposure assigns a value of unknown shape referring to the field itself to the mutable fields reachable from the + * unit's interface (see InterfaceExposure); such a field must declare its type. Fields that merely receive unknown + * values from this unit's own exposed parameters do not need to: their representation is already the unknown one. + * Record fields cannot declare types; their exposed representation is the unknown one. + */ + private def checkExposedMutableField(td: TermDefinition): Unit = + // A constructor parameter declares the type of the field it defines. + val declared = td.tsym.decl match + case S(p: Param) => p.sign.isDefined || p.modulefulness.isModuleful + case _ => td.sign.isDefined || td.modulefulness.isModuleful + val inputs = summon[State].newResolverState.erasureInputs + if config.language.useNewResolution && (td.k is syntax.MutVal) && !declared && !Inheritance.isExternal(td) + && inputs.isPrepared(td.tsym) then + val exposure = inputs.shapes(td.tsym).iterator.map(_.applicationHead._1).collectFirst: + case unknown: UnknownValueShape if unknown.fromPublicInterface && (unknown.source match + case ref: MemberRef => ref.sym is td.bsym + case _ => false) => unknown + exposure.foreach: unknown => + raise(ErrorReport(unknown.provenance.diagnostic( + msg"Mutable field '${td.tsym.nme}' is exposed to other compilation units, which can assign to it; annotate its type", + td.tsym.toLoc), source = Diagnostic.Source.Compilation)) + object Erasure: /** Prelude declarations use the same pass even though they have no executable program. */ def apply(root: Statement)(using Config, Ctx, State)(using Raise): Erasure = diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala index f6ea3bc447..3917f3b46f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala @@ -163,6 +163,7 @@ object EtaExpansion: tl: TL, raise: Raise, eState: Elaborator.State, + eCtx: Elaborator.Ctx, symbolPrinter: SymbolPrinter, ): Program = cfg.flowBasedOpt.fold(p): FlowAnalysisBasedRewrite.rewriteWith(p, _, dpe = false, dce = false) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala index b6bcbd0606..918fcd136c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala @@ -1,7 +1,7 @@ package hkmc2 package codegen -import scala.collection.mutable.{Set as MutSet, Buffer} +import scala.collection.mutable.{Set as MutSet, Buffer, LinkedHashMap} import hkmc2.utils.*, shorthands.* import utils.* @@ -20,6 +20,40 @@ class FlowAnalysisBasedRewrite( private case class EtaParamList(params: LoweredParamList, args: Ls[Arg]) + // the targets of each param list that eta expansion appends to `id`, with the target params dpe eliminates + private def etaTargetLists(id: ConcreteFunId, existingParams: Ls[LoweredParamList]): Ls[(EtaTargets, Set[Int])] = + def eliminableParamsOf(targets: EtaTargets): Set[Int] = + val paramInfos = targets.prodFuns.map(_.paramInfo) + assert(paramInfos.forall(_ === targets.paramInfo), + s"eta expansion level disagrees with its targets on arity: ${targets.pp} -> $paramInfos") + val eliminable = targets.prodFuns.map: target => + deadParamElimSolver.eliminableParams(target.concreteId) + assert(eliminable.size <= 1, + s"eta expansion targets disagree on eliminable parameters: " + + s"${targets.pp} -> $eliminable") + eliminable.headOption.getOrElse(Set.empty) + end eliminableParamsOf + + etaExpansionSolver.etaExpandedFunShape.get(id).toList.flatMap: targetShape => + val existingShape = existingParams.map: pl => + (pl.params.size, pl.paramCountLB, pl.restParam.isDefined) + if targetShape.map(_.paramInfo).startsWith(existingShape) then + targetShape.drop(existingShape.size).map(targets => targets -> eliminableParamsOf(targets)) + else + lastWords("not the same shape?") + + + // an eta param stands for the `i`-th param of every target, so it takes all their types + private def etaParamType(targets: EtaTargets, i: Int): Opt[ErasedValueType] = + // the params of the targets' param lists, read from their definitions, which keep up with lifting + def targetParams(target: ProdFun): Ls[Param] = target.exprId match + case (fun: TermSymbol, idx: Int) => pre.res.funSymToFunDefn(fun).params(idx).params + case lamId: ResultId => lamId.getResult match + case lam: Lambda => lam.params.params + case other => lastWords(s"expected lambda result, got $other") + val types = targets.prodFuns.toList.map(targetParams(_)(i).sym.erasedType) + if types.forall(_.isDefined) then S(ErasedType.Union.mk(types.flatten)) else N + class Rewriter(instId: InstantiationId, refreshSymbolMappingForCopy: Opt[collection.Map[LocalVarSymbol, LocalVarSymbol]]) extends InstantiationRewriter(instId, refreshSymbolMappingForCopy): // dead parameter elimination states and helper functions @@ -66,45 +100,25 @@ class FlowAnalysisBasedRewrite( finally activeEtaArgss = saved private def etaParamLists(id: ConcreteFunId, existingParams: Ls[LoweredParamList]): Ls[EtaParamList] = - - - def eliminableParamsOf(targets: EtaTargets): Set[Int] = - val paramInfos = targets.prodFuns.map(_.paramInfo) - assert(paramInfos.forall(_ === targets.paramInfo), - s"eta expansion level disagrees with its targets on arity: ${targets.pp} -> $paramInfos") - val eliminable = targets.prodFuns.map: target => - deadParamElimSolver.eliminableParams(target.concreteId) - assert(eliminable.size <= 1, - s"eta expansion targets disagree on eliminable parameters: " + - s"${targets.pp} -> $eliminable") - eliminable.headOption.getOrElse(Set.empty) - end eliminableParamsOf - - etaExpansionSolver.etaExpandedFunShape.get(id).toList.flatMap: targetShape => - val existingShape = existingParams.map: pl => - (pl.params.size, pl.paramCountLB, pl.restParam.isDefined) - if targetShape.map(_.paramInfo).startsWith(existingShape) then - targetShape.drop(existingShape.size).zipWithIndex.map: - case (targets, idx) => - val eliminable = eliminableParamsOf(targets) - def etaParam(name: Str): Param = - Param.simple(new VarSymbol(new Tree.Ident(s"eta$$$idx$$$name"), erasedType = N)) - // * The expanded parameters corresponding to optional parameters of the targets must - // * remain optional, since callers may omit their arguments. - def optionalEtaParam(name: Str): Param = - val sym = new VarSymbol(new Tree.Ident(s"eta$$$idx$$$name"), erasedType = N) - Param(FldFlags.empty.copy(opt = true), sym, N, Modulefulness.none)(N) - val params = (0 until targets.paramCount).iterator.filterNot(eliminable).map: i => - if i < targets.requiredParamCount then etaParam(i.toString) else optionalEtaParam(i.toString) - .toList - val restParam = Option.when(targets.hasRestParam)(etaParam("rest")) - EtaParamList( - LoweredParamList(ParamListFlags.empty, params, restParam)(N), - params.map(p => Arg(N, p.sym.asSimpleRef)) ::: - restParam.toList.map(p => Arg(S(SpreadKind.Eager), p.sym.asSimpleRef)), - ) - else - lastWords("not the same shape?") + etaTargetLists(id, existingParams).zipWithIndex.map: + case ((targets, eliminable), idx) => + def etaParam(name: Str, tpe: Opt[ErasedValueType]): Param = + Param.simple(new VarSymbol(new Tree.Ident(s"eta$$$idx$$$name"), erasedType = tpe)) + // * The expanded parameters corresponding to optional parameters of the targets must + // * remain optional, since callers may omit their arguments. + def optionalEtaParam(name: Str, tpe: Opt[ErasedValueType]): Param = + val sym = new VarSymbol(new Tree.Ident(s"eta$$$idx$$$name"), erasedType = tpe) + Param(FldFlags.empty.copy(opt = true), sym, N, Modulefulness.none)(N) + val params = (0 until targets.paramCount).iterator.filterNot(eliminable).map: i => + if i < targets.requiredParamCount then etaParam(i.toString, etaParamType(targets, i)) + else optionalEtaParam(i.toString, etaParamType(targets, i)) + .toList + val restParam = Option.when(targets.hasRestParam)(etaParam("rest", N)) + EtaParamList( + LoweredParamList(ParamListFlags.empty, params, restParam)(N), + params.map(p => Arg(N, p.sym.asSimpleRef)) ::: + restParam.toList.map(p => Arg(S(SpreadKind.Eager), p.sym.asSimpleRef)), + ) private def etaCall(base: Path): Result = Call(base, activeEtaArgss.ne_!)(CallMetadata.mlsFunWithEffect, N) @@ -262,11 +276,42 @@ class FlowAnalysisBasedRewrite( override def applyFunDefn(fun: FunDefn): FunDefn = rewrittenInPlace.get(fun.dSym) match case S((params, body)) => - if (params is fun.params) && (body is fun.body) then fun - else FunDefn(fun.owner, fun.sym, fun.dSym, params, body)(fun.configOverride, fun.annotations) + val dSym2 = fun.dSym.subst + if (params is fun.params) && (body is fun.body) && (dSym2 is fun.dSym) then fun + else FunDefn(fun.owner, fun.sym, dSym2, params, body)(fun.configOverride, fun.annotations) case N => super.applyFunDefn(fun) def otherNewFunDefns: Iterable[FunDefn] = Nil + + def retypedVariables: LinkedHashMap[(InstantiationId, ValueSymbol), ErasedValueType] = LinkedHashMap.empty + + // a function is retyped where dpe drops some of its params or eta expansion appends param lists to it; + // its params are read from its definition, which keeps up with lifting + lazy val retypedFunctions: LinkedHashMap[(InstantiationId, TermSymbol), ErasedFuncType] = + val res = LinkedHashMap.empty[(InstantiationId, TermSymbol), ErasedFuncType] + val funIds = + deadParamElimSolver.eliminableParamsById.keysIterator ++ etaExpansionSolver.etaExpandedFunShape.keysIterator + for case ConcreteId((fun: TermSymbol, _), instId) <- funIds if !res.contains(instId -> fun) do + val params = pre.res.funSymToFunDefn(fun).params + val kept = params.zipWithIndex.map: (pl, i) => + val eliminable = deadParamElimSolver.eliminableParams(ConcreteId((fun, i), instId)) + pl.params.zipWithIndex.collect: + case (p, j) if !eliminable(j) => p.sym.erasedType + val eta = etaTargetLists(ConcreteId((fun, 0), instId), params) + val etaLists = eta.map: (targets, eliminable) => + (0 until targets.paramCount).filterNot(eliminable).map(etaParamType(targets, _)).toList + val ret = eta.lastOption.fold(fun.declaredResultType): (targets, _) => + val results = targets.prodFuns.toList.map: target => + target.exprId match + case (targetFun: TermSymbol, idx: Int) + if idx == pre.res.funSymToFunDefn(targetFun).params.size - 1 + && !etaExpansionSolver.etaExpandedFunShape.contains( + ConcreteId((targetFun, 0), target.concreteId.instId)) => + targetFun.declaredResultType + case _ => N + if results.forall(_.isDefined) then S(ErasedType.Union.mk(results.flatten)) else N + res(instId -> fun) = ErasedType.FuncRef(S(false), kept ::: etaLists, ret) + res // ====== end: implements the abstract members of `PolyInstantiationRewrite` ====== end FlowAnalysisBasedRewrite @@ -280,6 +325,7 @@ object FlowAnalysisBasedRewrite: tl: TL, raise: Raise, eState: Elaborator.State, + eCtx: Elaborator.Ctx, symbolPrinter: SymbolPrinter, ): Program = cfg.flowBasedOpt.fold(p)(rewriteWith(p, _)) @@ -295,6 +341,7 @@ object FlowAnalysisBasedRewrite: tl: TL, raise: Raise, eState: Elaborator.State, + eCtx: Elaborator.Ctx, symbolPrinter: SymbolPrinter, ): Program = def mkTl(prefix: Str, debug: Bool) = diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala index 30a511c7f9..5291168073 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala @@ -608,9 +608,11 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): (sym -> varSym, p, vd) + val ctorSym = ClassCtorSymbol(syntax.Fun, N, clsSym) + ctorSym.erasedType = N val defn = ClsLikeDefn( None, clsSym, BlockMemberSymbol(nme, Nil), - S(ClassCtorSymbol(syntax.Fun, N, clsSym)), + S(ctorSym), syntax.Cls, N, LoweredParamList.plain(sortedVars.iterator.map(_.param).toList)(N) :: Nil, None, Nil, Nil, diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 3c0acc12f4..307c0e8ead 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -346,13 +346,15 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu // Assign(td.sym, r, // term(st.Blk(stats, res))(k))) Define( - ValDefn(td.tsym, td.sym, castTo(r, td.tsym.erasedType, bod.toLoc))(cfgOverride, td.annotations), + // A field synthesized from a constructor parameter has an unlocated body (the parameter + // reference); its declared name then locates diagnostics about the field's representation. + ValDefn(td.tsym, td.sym, castTo(r, td.tsym.erasedType, bod.toLoc.orElse(td.tsym.toLoc)))(cfgOverride, td.annotations), blockImpl(stats, res), ) case syntax.Fun => val cfgOverride = definitionConfigOverride(td.extraAnnotations) val (paramLists, bodyBlock) = debugLowering(td.extraAnnotations): - setupFunctionOrByNameDef(td.params, bod, S(td.sym.nme), td.tsym.declaredResultType) + setupFunctionOrByNameDef(td.params, bod, S(td.sym.nme), summon[Erasure].bodyResultType(td)) Define(FunDefn(td.owner, td.sym, td.tsym, paramLists, bodyBlock)(cfgOverride, td.annotations), blockImpl(stats, res)) case syntax.LetBind | syntax.HandlerBind | syntax.RecordField => fail: @@ -595,7 +597,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case (ps :: remainingParams, args :: remainingArgs) => def next(as: Ls[Arg]): Block = zipArgs(remainingParams, remainingArgs, as :: acc, mayRaiseEffects) if isConstructor then lowerConstructorArgs(args, ps, isMlsFun)(next) - else lowerArgs(args, expectedParamTypes(ps)): as => + else lowerArgs(args, expectedParamTypes(ps, args)): as => // Trailing parameters are selected from the end of the source argument list. Padding // that list here would shift them; their extraction prologue supplies missing nulls. if useMlsDefaults && ps.variadic.forall(_.trailing.isEmpty) && as.forall(_.spread.isEmpty) then @@ -706,7 +708,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case (arg :: rest, _ :: params) => loop(rest, params, arg :: acc) case _ => k(acc.reverse ::: args) loop(args, ps.params, Nil) - lowerArgs(arg, expectedParamTypes(ps)): args => + lowerArgs(arg, expectedParamTypes(ps, arg)): args => if !ps.params.exists(_.flags.opt) then k(args) else if args.forall(_.spread.isEmpty) then val padded = if isMls then OptionalParams.pad(ps.params, args).getOrElse(args) @@ -1387,7 +1389,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu N case Some(bod) => reportAnnotations(td, td.annotations) - val (paramLists, bodyBlock) = setupFunctionDef(td.params, bod, S(td.sym.nme), td.tsym.declaredResultType) + val (paramLists, bodyBlock) = setupFunctionDef(td.params, bod, S(td.sym.nme), summon[Erasure].bodyResultType(td)) S(Handler(td.sym, resumeSym, paramLists, bodyBlock)) }.collect{ case Some(v) => v } loweringCtx.collectScopedSym(lhs) @@ -1612,11 +1614,11 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu subTerm_nonTail(rhs): value => AssignField(ref, Tree.Ident("value"), value, k(value))(N) - case Mut(Rcd(mut, stats)) => // TODO: warn when in statement position + case Mut(rcd: st.Rcd) => // TODO: warn when in statement position // * Note: I don't think this is supposed to happen... - block(stats, L((mut, Nil, trm.toLoc)))(k) - case Rcd(mut, stats) => // TODO: warn when in statement position - block(stats, L((mut, Nil, trm.toLoc)))(k) + block(rcd.stats, L((allocatesMutable(rcd), Nil, trm.toLoc)))(k) + case rcd: st.Rcd => // TODO: warn when in statement position + block(rcd.stats, L((allocatesMutable(rcd), Nil, trm.toLoc)))(k) case Missing => fail: ErrorReport( @@ -1805,7 +1807,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu .flatMap: td => td.body.map: bod => val (paramLists, bodyBlock) = debugLowering(td.extraAnnotations): - setupFunctionDef(td.params, bod, S(td.sym.nme), td.tsym.declaredResultType) + setupFunctionDef(td.params, bod, S(td.sym.nme), summon[Erasure].bodyResultType(td)) reportAnnotations(td, td.extraAnnotations) val cfgOverride = definitionConfigOverride(td.extraAnnotations) FunDefn(td.owner, td.sym, td.tsym, paramLists, bodyBlock)(cfgOverride, td.annotations) @@ -1914,17 +1916,40 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu * - Otherwise, `r` is returned unchanged. * * An absent `expected` is an unannotated slot, which holds the top reference type rather than no type at all. + * When the caller has no better location for the coercion (e.g., an implicit return), diagnostics point at + * the coerced value itself. */ def castTo[R <: Result](r: R, expected: Opt[ErasedType], loc: Opt[Loc]): (R | Cast) = - r.coerceTo(expected.getOrElse(ErasedType.Unknown), loc) + r.coerceTo(expected.getOrElse(ErasedType.Unknown), loc.orElse(r.toLoc)) + + /** Whether a record literal is allocated as a mutable object, rather than frozen: a mutable record (`mut {...}`) + * and a record with a field declared `mut` (see `RcdField`). The other fields of the latter are immutable as well, + * but are not frozen at run time: resolution assumes that they are never assigned, which type checking will ensure. + */ + private def allocatesMutable(rcd: st.Rcd): Bool = rcd.mut || rcd.stats.exists: + case field: RcdField => field.mut + case _ => false /** The declared erased type of the field a selection resolves to, if it is an annotated `TermSymbol`. */ private def fieldErasedType(s: Opt[Symbol]): Opt[ErasedType] = s.collect { case t: TermSymbol => t.erasedType }.flatten - /** The declared erased types of a parameter list's fixed parameters (excluding rest params). */ - private def expectedParamTypes(ps: ParamList): Ls[Opt[ErasedType]] = - ps.params.map(_.sym.erasedType) + /** Match fixed argument positions to their source slots before trailing parameters are packed + * into a rest array. With spreads, only the required prefix has a known alignment; the + * extraction prologue coerces the remaining values when it binds their destination slots. + */ + private def expectedParamTypes(ps: ParamList, args: Term): Ls[Opt[ErasedType]] = + if ps.trailingParams.isEmpty then ps.params.map(_.sym.erasedType) + else args match + case Tup(fields) if fields.forall { + case Fld(FldFlags.benign(), _, N) => true + case _ => false + } => + val leading = ps.params.take((fields.length - ps.trailingParams.length).max(0)) + val trailing = ps.trailingParams.takeRight(fields.length - leading.length) + leading.map(_.sym.erasedType) ::: + List.fill(fields.length - leading.length - trailing.length)(N) ::: trailing.map(_.sym.erasedType) + case _ => ps.params.take(ps.leadingRequiredCount).map(_.sym.erasedType) def program(main: st.Blk, symbolsToPreserve: Set[ScopedSymbol]): Program = @@ -2020,7 +2045,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu :: trailing.zipWithIndex.map((p, i) => p.sym -> at(args, i - k)) (optional ::: variadicParam :: trailing).foreach(p => loweringCtx.collectScopedSym(p.sym)) val extractedRest = extractions.foldRight(body): - case ((sym, res), rest) => Assign(sym, res, rest) + case ((sym, res), rest) => Assign(sym, castTo(res, sym.erasedType, sym.toLoc), rest) // * Exclude trailing arguments before checking optional slot presence, so they cannot // * accidentally fill a missing optional parameter. val extracted = if optional.isEmpty then extractedRest else diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/OptionalParams.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/OptionalParams.scala index ca2ebd430c..16eee5a790 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/OptionalParams.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/OptionalParams.scala @@ -3,7 +3,7 @@ package codegen import hkmc2.utils.*, shorthands.* import semantics.* -import semantics.Elaborator.State +import semantics.Elaborator.{Ctx, State} import syntax.{Tree, SpreadKind} @@ -45,17 +45,22 @@ object OptionalParams: /** Bind a slot from an already evaluated argument tuple. The tuple-length guard prevents an * out-of-bounds read from manufacturing an uninitialized value for an absent optional argument. */ - def bindFromTuple(args: Path, index: Int, sym: LocalVarSymbol, optional: Bool, rest: Block): Block = + def bindFromTuple(args: Path, index: Int, sym: LocalVarSymbol, optional: Bool, rest: Block) + (using Config, Ctx, State, Raise): Block = val value = DynSelect(args, Value.Lit(Tree.IntLit(index))(N), DynSelectKind.ArrayIdx)(sym.toLoc) - if !optional then Assign(sym, value, rest) + // Tuple reads are dynamically typed even when the destination slot has a precise representation. + // Preserve that boundary for constructor fields, extracted parameters, and tail-call assignments. + def bind(value: Result, rest: Block): Block = + Assign(sym, value.coerceTo(sym.erasedType.getOrElse(ErasedType.Unknown), sym.toLoc), rest) + if !optional then bind(value, rest) else assert(index >= 0, "Optional slots are indexed from the start of the argument tuple") - Match(args, Case.Tup(index + 1, true) -> Assign(sym, value, End()) :: Nil, - S(Assign(sym, Value.Lit(Tree.UnitLit(true))(sym.toLoc), End())), rest) + Match(args, Case.Tup(index + 1, true) -> bind(value, End()) :: Nil, + S(bind(Value.Lit(Tree.UnitLit(true))(sym.toLoc), End())), rest) /** Evaluate a constructor slot once before optimizers can replace field reads with arguments. */ def argument(value: Path)(k: Path => Block)(using State): Block = value match case _: Value.Lit => k(value) case _ => - val slot = TempSymbol(N, erasedType = N, "optionalArg") + val slot = TempSymbol(N, erasedType = value.erasedValueType, "optionalArg") Scoped(Set(slot), Assign(slot, value, k(slot.asSimpleRef))) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/PolyInstantiationRewrite.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/PolyInstantiationRewrite.scala index a0b75f2afb..a6a93f53bb 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/PolyInstantiationRewrite.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/PolyInstantiationRewrite.scala @@ -41,6 +41,12 @@ abstract class PolyInstantiationRewrite(val constraintSolver: FlowConstraintSolv // definitions this rewrite introduces besides the specialized copies def otherNewFunDefns: Iterable[FunDefn] + + // updated erased value types + def retypedVariables: LinkedHashMap[(InstantiationId, ValueSymbol), ErasedValueType] + + // updated erased function types + def retypedFunctions: LinkedHashMap[(InstantiationId, TermSymbol), ErasedFuncType] // ====== the abstract members ====== @@ -51,6 +57,34 @@ abstract class PolyInstantiationRewrite(val constraintSolver: FlowConstraintSolv given eState: Elaborator.State = constraintSolver.eState given pre: FlowPreAnalyzer = constraintSolver.preAnalyzer + // the instantiation id where the original definition of a root function is rewritten in place + protected final def inPlaceInstId(fun: TermSymbol): InstantiationId = + collector.synthesizedInstIdToFunSym.collectFirst: + case (instId, f) if f is fun => instId + .getOrElse(Nil) + + private lazy val rewrittenFunctionSymbols: Map[TermSymbol, TermSymbol] = + pre.res.rootFunDefns.keysIterator.flatMap: sym => + retypedFunctions.get(inPlaceInstId(sym) -> sym).collect: + case tpe if !sym.erasedType.contains(tpe) => sym -> SymbolRefresher.copyTermSymbol(sym, sym.owner, S(tpe)) + .toMap + + private lazy val retypedLocals: LinkedHashMap[(InstantiationId, ValueSymbol), ErasedType] = + LinkedHashMap.from[(InstantiationId, ValueSymbol), ErasedType](retypedVariables) ++ retypedFunctions.filterNot: (key, _) => + pre.res.rootFunDefns.contains(key._2) + + private lazy val sharedRetypedSymbols: LinkedHashMap[(InstantiationId, ValueSymbol), ValueSymbol] = + retypedLocals.map((key, tpe) => key -> retypedSymbol(key._2, tpe)) + + private def retypedSymbol(previous: Symbol, tpe: ErasedType): ValueSymbol = (previous, tpe) match + case (v: VarSymbol, _) if v.erasedType.contains(tpe) => v + case (v: VarSymbol, tpe: ErasedValueType) => SymbolRefresher.copyVarSymbol(v, S(tpe)) + case (t: TempSymbol, _) if t.erasedType.contains(tpe) => t + case (t: TempSymbol, _) => SymbolRefresher.copyTempSymbol(t, S(tpe)) + case (t: TermSymbol, _) if t.erasedType.contains(tpe) => t + case (t: TermSymbol, _) => SymbolRefresher.copyTermSymbol(t, t.owner, S(tpe)) + case _ => lastWords(s"cannot give $previous the erased type $tpe") + final def apply(): Program = if !solvers.exists(_.hasWorkToDo) then pre.pgrm @@ -59,14 +93,16 @@ abstract class PolyInstantiationRewrite(val constraintSolver: FlowConstraintSolv if newBody is pre.pgrm.main then pre.pgrm else Program(pre.pgrm.imports, newBody) - val newPolyFnSyms : LinkedHashMap[InstantiationId, Map[TermSymbol, (BlockMemberSymbol, TermSymbol)]] = + lazy val newPolyFnSyms : LinkedHashMap[InstantiationId, Map[TermSymbol, (BlockMemberSymbol, TermSymbol)]] = val res = LinkedHashMap.empty[InstantiationId, Map[TermSymbol, (BlockMemberSymbol, TermSymbol)]] def funSymsToBeCopiedAt(path: InstantiationId): Iterator[TermSymbol] = collector.synthesizedInstIdToFunSym.get(path) match case S(entry) => collector.funToSccGroups(entry).iterator.filterNot(_ is entry) case N => collector.funToSccGroups(path.last.getReferredFun.get).iterator for - instId <- solvers.iterator.flatMap(_.polyInstIds) + // a call into an instance whose types change needs a copy even where none of its code changes, + // as the original definition keeps the original types + instId <- solvers.iterator.flatMap(_.polyInstIds) ++ retypedVariables.keysIterator.map(_._1) ++ retypedFunctions.keysIterator.map(_._1) path <- instId.inits if path.nonEmpty && !res.contains(path) copied = funSymsToBeCopiedAt(path) @@ -77,7 +113,7 @@ abstract class PolyInstantiationRewrite(val constraintSolver: FlowConstraintSolv val name = path.mkFunName + s"$$${f.nme}" f -> ( new BlockMemberSymbol(name, Nil, true), - new TermSymbol(Fun, N, Tree.Ident(name), erasedType = f.erasedType)) + new TermSymbol(Fun, N, Tree.Ident(name), erasedType = retypedFunctions.get(path -> f).orElse(f.erasedType))) .toMap res end newPolyFnSyms @@ -111,18 +147,28 @@ abstract class PolyInstantiationRewrite(val constraintSolver: FlowConstraintSolv // the rewriter of a copy of `params` and `body`, which maps symbol defined in `initMapping` // and refreshes symbols bound in `body` so that a copy never rebinds an original symbol protected final def mkCopyRewriter(instId: InstantiationId, params: Ls[LoweredParamList], body: Block, initMapping: Map[LocalVarSymbol, LocalVarSymbol]): Rewriter = - val mapping = MutMap.from(initMapping) - val walker = SymbolRefresherWalker(mapping.asInstanceOf) + val mapping = MutMap.from[Symbol, Symbol](initMapping) + val walker = SymbolRefresherWalker(mapping) params.foreach(walker.refreshParamList) walker.applyBlock(body) - mkRewriter(instId, S(mapping)) + for + case sym: ValueSymbol <- mapping.keys.toList.sortBy(_.uid) + tpe <- retypedLocals.get(instId -> sym).orElse(retypedLocals.get(Nil -> sym)) + do sym match + case sym: LocalVarSymbol if initMapping.contains(sym) => + assert(initMapping(sym).erasedType.contains(tpe), s"$sym is mapped to ${initMapping(sym)}, which lacks its type $tpe") + case _ => mapping(sym) = retypedSymbol(mapping(sym), tpe) + mkRewriter(instId, S(mapping.asInstanceOf[collection.Map[LocalVarSymbol, LocalVarSymbol]])) // rewrites the program as seen from one instantiation path `instId`: in place if `refreshSymbolMappingForCopy` is `N`, // otherwise into a copy whose symbols are refreshed by the map // helps subclass compute the symbol for the instantiated fun defns protected abstract class InstantiationRewriter(val instId: InstantiationId, val refreshSymbolMappingForCopy: Opt[collection.Map[LocalVarSymbol, LocalVarSymbol]]) - extends SymbolMapTransformer(refreshSymbolMappingForCopy.getOrElse(Map.empty).asInstanceOf): + extends SymbolMapTransformer( + Map.from[Symbol, Symbol](rewrittenFunctionSymbols) + ++ sharedRetypedSymbols.collect { case ((id, sym), retyped) if id.isEmpty || id == instId => sym -> retyped } + ++ refreshSymbolMappingForCopy.getOrElse(Map.empty)): def rewriteFunDefn(fun: FunDefn): RewrittenFunDefn diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/SymbolRefresher.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/SymbolRefresher.scala index 8476e5c206..6786b24282 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/SymbolRefresher.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/SymbolRefresher.scala @@ -16,12 +16,10 @@ class SymbolRefresherWalker(mapping: MutMap[Symbol, Symbol])(using State) extend mapping(k) = v private def refreshTempSymbol(s: TempSymbol) = - assertUpdate(s, new TempSymbol(s.trm, s.erasedType, s.nme)) + assertUpdate(s, SymbolRefresher.copyTempSymbol(s, s.erasedType)) private def refreshVarSymbol(s: VarSymbol) = - val ns = new VarSymbol(s.id, s.erasedType) - ns.sourceAliases = s.sourceAliases - assertUpdate(s, ns) + assertUpdate(s, SymbolRefresher.copyVarSymbol(s, s.erasedType)) private def refreshBlockMemberSymbol(s: BlockMemberSymbol) = val ns = new BlockMemberSymbol(s.nme, s.trees, s.nameIsMeaningful) @@ -33,9 +31,8 @@ class SymbolRefresherWalker(mapping: MutMap[Symbol, Symbol])(using State) extend private def refreshTermSymbol(s: TermSymbol) = // Inner symbol (if present) must be traversed at this point. - val ns = new TermSymbol(s.k, s.owner.map(o => mapping.getOrElse(o, o).asInstanceOf[InnerSymbol]), s.id, s.erasedType) - ns.sourceAliases = s.sourceAliases - assertUpdate(s, ns) + val owner = s.owner.map(o => mapping.getOrElse(o, o).asInstanceOf[InnerSymbol]) + assertUpdate(s, SymbolRefresher.copyTermSymbol(s, owner, s.erasedType)) private def refreshClassSymbol(s: ClassSymbol) = val ns = new ClassSymbol(s.tree, s.id) @@ -55,9 +52,11 @@ class SymbolRefresherWalker(mapping: MutMap[Symbol, Symbol])(using State) extend assertUpdate(s, new TopLevelSymbol(s.nme)) private def refreshClassCtorSymbol(s: ClassCtorSymbol) = - assertUpdate(s, new ClassCtorSymbol(s.k, + val ns = new ClassCtorSymbol(s.k, s.owner.map(o => mapping.getOrElse(o, o).asInstanceOf[InnerSymbol]), - mapping.getOrElse(s.associatedCls, s.associatedCls).asInstanceOf[ClassSymbol])) + mapping.getOrElse(s.associatedCls, s.associatedCls).asInstanceOf[ClassSymbol]) + ns.erasedType = s.erasedType + assertUpdate(s, ns) def refreshParamList(pl: LoweredParamList) = for @@ -139,6 +138,20 @@ class SymbolRefresherWalker(mapping: MutMap[Symbol, Symbol])(using State) extend object SymbolRefresher: + // fresh copies that keep a symbol's source names but hold the given erased type + def copyVarSymbol(s: VarSymbol, erasedType: Opt[ErasedValueType])(using State): VarSymbol = + val ns = new VarSymbol(s.id, erasedType) + ns.sourceAliases = s.sourceAliases + ns + + def copyTempSymbol(s: TempSymbol, erasedType: Opt[ErasedType])(using State): TempSymbol = + new TempSymbol(s.trm, erasedType, s.nme) + + def copyTermSymbol(s: TermSymbol, owner: Opt[InnerSymbol], erasedType: Opt[ErasedType])(using State): TermSymbol = + val ns = new TermSymbol(s.k, owner, s.id, erasedType) + ns.sourceAliases = s.sourceAliases + ns + def initSymbolSubst(m: collection.Map[Symbol, Symbol]) = new SymbolSubst: override def mapBlockMemberSym(s: BlockMemberSymbol): BlockMemberSymbol = m.getOrElse(s, s).asInstanceOf[BlockMemberSymbol] diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala index 86077c27db..2468c4ee84 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Deforest.scala @@ -15,6 +15,10 @@ sealed abstract class FinalDest case class FinalDestMatch(dtor: CtorDtorId, sels: Set[CtorDtorId]) extends FinalDest case class FinalDestSel(dtors: Set[CtorDtorId], field: SelField) extends FinalDest +extension (f: SelField) def erasedFieldType(using Raise): Opt[ErasedValueType] = f match + case tSym: TermSymbol => tSym.mapErasedValueType + case _: Int => N + class DeforestFusionSolver(val constraintSolver: FlowConstraintSolver)(using Config, Elaborator.Ctx) extends FlowAnalysisSolverResult: given preAnalyzer: FlowPreAnalyzer = constraintSolver.preAnalyzer @@ -42,6 +46,31 @@ class DeforestFusionSolver(val constraintSolver: FlowConstraintSolver)(using Con selExpr.getResult.matches: case TrackableSelect(from, _, _) => from === dtor.exprId.getResult + /** Decide only predicates determined by a concrete producer. An incomplete parent chain does + * not prove a class test false, and a class instance's type does not determine its tuple length. + */ + private def matches(ctor: CtorCls, cse: Case): Opt[Bool] = (ctor, cse) match + case (cls: TypeSymbol, Case.Cls(expected: TypeSymbol, _)) => ErasedType.isSubtypeOf(cls, expected) + case (_: Int, Case.Cls(expected: TypeSymbol, _)) => ErasedType.isSubtypeOf(summon[Elaborator.Ctx].builtins.Array, expected) + case (length: Int, Case.Tup(expected, inf)) => S(if inf then length >= expected else length == expected) + // Concrete producers are objects or tuples, never literal values. + case (_, Case.Lit(_)) => S(false) + case _ => N + + /** Both fusion eligibility and branch extraction use this ordered decision. In particular, a + * later matching arm cannot justify fusion when an earlier arm's predicate remains unknown. + */ + def selectedBranch(ctor: CtorCls, dtor: CtorDtorId): Opt[Block] = + val matched = preAnalyzer.res.matchScrutToMatchBlock(dtor.exprId) + def loop(arms: Ls[Case -> Block]): Opt[Block] = arms match + case (cse, body) :: rest => matches(ctor, cse) match + case S(true) => S(body) + case S(false) => loop(rest) + case N => N + // With no default, a failed match falls through to its rest block. + case Nil => S(matched.dflt.getOrElse(End())) + loop(matched.arms) + val finalCtorDests = LinkedHashMap.empty[CtorDtorId, FinalDest] val finalDtorSrcs = LinkedHashMap.empty[CtorDtorId, Set[CtorDtorId]] val fusingCtorInfo = MutMap.empty[CtorDtorId, Ctor] @@ -56,17 +85,24 @@ class DeforestFusionSolver(val constraintSolver: FlowConstraintSolver)(using Con ctor.instId :: dtor.instId :: Nil locally { - def mergeDests(dests: Set[ConcreteCtorConsumer | MarkerConsStrat]): Opt[FinalDest] = + def mergeDests(ctor: Ctor, dests: Set[ConcreteCtorConsumer | MarkerConsStrat]): Opt[FinalDest] = def selsSelectingTheSameSymbol(sels: Set[FieldSel]) = sels.map(s => s.field).size == 1 - if dests.exists(_.isInstanceOf[MarkerConsStrat]) then + val canSkipConstruction = ctor.exprId.getResult match + // Rewriting replays these arguments, but drops the constructor body and qualifier. + // A stale source parameter layout must not truncate the replayed argument list. + case CtorProducer(_, args, selectedFrom) => args.length == ctor.args.length && + selectedFrom.forall(_.isPure) && ConstructorElision.isHarmless(ctor.ctor) + case _ => false + if !canSkipConstruction || dests.exists(_.isInstanceOf[MarkerConsStrat]) then N else val (dtors, sels) = dests.partitionMap: case d: Dtor => Left(d) case fs: FieldSel => Right(fs) case _ => die - if dtors.size == 0 && selsSelectingTheSameSymbol(sels) then + if sels.nonEmpty && !ConstructorElision.preservesParameterFields(ctor) then N + else if dtors.size == 0 && selsSelectingTheSameSymbol(sels) then S(FinalDestSel( sels.map(_.concreteId), sels.head.field @@ -77,7 +113,7 @@ class DeforestFusionSolver(val constraintSolver: FlowConstraintSolver)(using Con if selAndDtorIsSameConsumer( dtor.concreteId, sels.map(s => s.concreteId) - ) then + ) && selectedBranch(ctor.ctor, dtor.concreteId).isDefined then S(FinalDestMatch( dtor.concreteId, sels.map(_.concreteId) @@ -86,24 +122,48 @@ class DeforestFusionSolver(val constraintSolver: FlowConstraintSolver)(using Con end mergeDests val prodRoots = - for - ctor <- constraintSolver.ctorsWithDests - if mergeDests(ctor.dests.toSet).isEmpty - yield ctor + val unconsumed = constraintSolver.collector.allRealCtors.iterator.filter: ctor => + ctor.instantiationId.isDefined && ctor.dests.isEmpty + val unmergeable = + for + ctor <- constraintSolver.ctorsWithDests + if mergeDests(ctor, ctor.dests.toSet).isEmpty + yield ctor + unconsumed ++ unmergeable val consRoots = for dtor <- constraintSolver.consumersWithSrcs if dtor.srcs.contains(UnknownProd) yield dtor - val result = FlowWebComputation[Ctor, ConcreteCtorConsumer]( - p => p.dests.collect: - case c: ConcreteCtorConsumer => c, - c => c.srcs.collect: - case p: Ctor => p, - prodRoots, - consRoots, - ) + val result = + // constructors stored in `c`'s field that must stay unfused when `c` is unfused, + // as `c`'s fields keep their declared types + def storedCtorsKeptUnfused(c: Ctor): Iterator[Ctor] = + for + (field, arg) <- c.args.iterator + stored <- arg match + case stored: Ctor => Iterator.single(stored) + case v: StratVar => v.lowerBounds.iterator.collect: + case stored: Ctor => stored + case _ => Iterator.empty + if mergeDests(stored, stored.dests.toSet) match + case S(_: FinalDestMatch) => + field.erasedFieldType.exists: tpe => + !ErasedType.needsCast( + ErasedType.Function(S(false)).canonicalize, + tpe.canonicalize).contains(false) + case _ => true + yield stored + FlowWebComputation[Ctor, ConcreteCtorConsumer]( + p => p.dests.collect: + case c: ConcreteCtorConsumer => c, + c => c.srcs.collect: + case p: Ctor => p, + storedCtorsKeptUnfused, + prodRoots, + consRoots, + ) val toRemoveCtor = result.markedProducers val toRemoveDtor = result.markedConsumers @@ -111,7 +171,7 @@ class DeforestFusionSolver(val constraintSolver: FlowConstraintSolver)(using Con ctor <- constraintSolver.ctorsWithDests if !toRemoveCtor(ctor) do - finalCtorDests(ctor.concreteId) = mergeDests(ctor.dests.toSet).get + finalCtorDests(ctor.concreteId) = mergeDests(ctor, ctor.dests.toSet).get fusingCtorInfo(ctor.concreteId) = ctor for dtor <- constraintSolver.consumersWithSrcs diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Rewrite.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Rewrite.scala index d1e9166754..ee3db3d6ce 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Rewrite.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Rewrite.scala @@ -12,11 +12,13 @@ import hkmc2.codegen.flowAnalysis.* -class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbols: Set[LocalVarSymbol])(using Raise) +class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbols: Set[LocalVarSymbol])(using Raise, Config, Elaborator.Ctx) extends PolyInstantiationRewrite(solver.constraintSolver): type MatchOrLabelId = ResultId | LabelSymbol - type BranchId = CtorDtorId -> Opt[CtorCls] + // The producer determines the branch closure's field parameters even when several producers + // select the same default body. Different constructor layouts cannot share that closure. + type BranchId = CtorDtorId -> CtorCls type LabelId = LabelSymbol -> InstantiationId type RestFunId = CtorDtorId | LabelId @@ -39,9 +41,48 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo extension (f: SelField) def fieldName: String = f match case tSym: TermSymbol => tSym.nme case n: Int => n.toString - extension (f: SelField) def erasedFieldType: Opt[ErasedValueType] = f match - case tSym: TermSymbol => tSym.mapErasedValueType - case _: Int => N + + + private def isFused(p: ProdStrat): Bool = p match + case c: Ctor => solver.finalCtorDests.contains(c.concreteId) + case _: (ProdFun | MarkerProdStrat) => false + case _: StratVar => lastWords("unexpected strat var") + + private def producersOf(values: ProdStrat): Ls[ProdStrat] = values match + case v: StratVar => v.lowerBounds.toList + case p => p :: Nil + + // compute the updated erased type of a local variable, a function param or a function return, + // using all the concrete producers that flows into that slot + private def slotTypeFromFlow(producers: Ls[ProdStrat], declared: Opt[ErasedValueType]): Opt[ErasedValueType] = + def go(producers: Ls[ProdStrat], declared: Opt[ErasedValueType], seen: Set[Ctor]): Opt[ErasedValueType] = + def producerType(p: ProdStrat): Opt[ErasedValueType] = p match + case c: Ctor => solver.finalCtorDests.get(c.concreteId) match + case S(_: FinalDestMatch) => S(ErasedType.Function(S(false))) + case S(FinalDestSel(_, field)) => + val fieldType = + if seen(c) then field.erasedFieldType + else go(producersOf(c.args.find(_._1 == field).get._2), field.erasedFieldType, seen + c) + S(fieldType.getOrElse(ErasedType.Unknown)) + case N => S(c.allocatedType) + case _: ProdFun => S(ErasedType.Function(S(false))) + case UnknownProd => N + case _: StratVar => lastWords("unexpected strat var") + val types = producers.map(producerType) + if types.forall(_.isEmpty) then declared + else S(ErasedType.Union.mk(types.map(_.orElse(declared).getOrElse(ErasedType.Unknown)))) + go(producers, declared, Set.empty) + + // N when the existing slot keeps its declaration, as no fused value reaches it + private def retypedSlotType(producers: Ls[ProdStrat], declared: Opt[ErasedValueType]): Opt[ErasedValueType] = + if producers.exists(isFused) then slotTypeFromFlow(producers, declared) else N + + // A proved cast can disappear when fusion changes its operand's representation. + // Keep other casts: even a check known to succeed can still narrow a physical anyref + // slot for Wasm. Unknown/failing casts expose their inputs and therefore prevent fusion. + private val fusedCasts = constraintSolver.castsWithSrcs.iterator.collect: + case c if c.succeeds && c.sources.exists(isFused) => c.concreteId + .toSet private def getParentLabelOrMatchesAndRestBefore( matchOrLabelId: MatchOrLabelId @@ -94,7 +135,7 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo val restFunSyms = LinkedHashMap.empty[RestFunId, (BlockMemberSymbol, TermSymbol)] // original bodies of a branch - val branchOriginalBodies = MutMap.empty[ResultId -> Opt[CtorCls], Block] + val branchOriginalBodies = MutMap.empty[ResultId -> CtorCls, Block] // original rest function bodies and their parent matches (if any) val restOriginalBodiesAndParentRest = MutMap.empty[MatchOrLabelId, Block -> Opt[MatchOrLabelId]] @@ -115,25 +156,14 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo val clsNme = selInfo.selectsFrom.ctorClsName fieldSym.getOrElseUpdate( selInfo.field, - new VarSymbol( - Tree.Ident(s"${clsNme}_${selInfo.field.fieldName}"), - erasedType = selInfo.field.erasedFieldType)) + new VarSymbol(Tree.Ident(s"${clsNme}_${selInfo.field.fieldName}"))) ) // ctor dest branch function computations - val matchBlk = pre.res.matchScrutToMatchBlock(dest._1) - val (whichBranch, whichBranchPreBody) = - val tmp = - val ctorCls = ctorInfo.ctor - matchBlk.arms - .find: (cse, _) => - cse match - case Case.Cls(cls, path) => cls === ctorCls - case Case.Tup(len, inf) => len === ctorCls - case _ => die - .map(b => ctorCls -> b._2) - tmp.map(_._1) -> - tmp.fold(matchBlk.dflt.get)(_._2) + val whichBranch = ctorInfo.ctor + val whichBranchPreBody = + solver.selectedBranch(ctorInfo.ctor, dest).getOrElse( + lastWords("Fusion requires a statically determined match branch")) val destBranchId: BranchId = dest -> whichBranch // identify the dest branchid for a ctor ctorWhichBranch(ctor) = destBranchId @@ -141,11 +171,11 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo branchFunSyms.getOrElseUpdate( destBranchId, locally: - val branchName = whichBranch.fold("_dflt")(c => s"_${c.ctorClsName}") + val branchName = s"_${whichBranch.ctorClsName}" val scrutName = dest._1.getReferredSym.nme val branchFnNme = s"${dest.instId.mkFunName}$$$scrutName$branchName" new BlockMemberSymbol(branchFnNme, Nil, true) - -> new TermSymbol(Fun, N, Tree.Ident(branchFnNme), erasedType = N) + -> new TermSymbol(Fun, N, Tree.Ident(branchFnNme)) ) // compute the function parameters corresponding to ctor fields of branch funs branchFunParamFieldSyms.getOrElseUpdate( @@ -160,7 +190,7 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo completeArgs.map: selField => selsInfos.get(selField) match case Some(selId) => branchSelSyms(selId) - case None => VarSymbol(Tree.Ident(s"_${selField.fieldName}"), erasedType = selField.erasedFieldType) + case None => VarSymbol(Tree.Ident(s"_${selField.fieldName}")) ) val (parents, _) = getParentLabelOrMatchesAndRestBefore(dest.exprId) @@ -174,7 +204,7 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo locally: val restFunName = dest.instId.mkFunName + s"$$${nme}_rest" new BlockMemberSymbol(restFunName, Nil, true) - -> new TermSymbol(Fun, N, Tree.Ident(restFunName), erasedType = N) + -> new TermSymbol(Fun, N, Tree.Ident(restFunName)) ) val (ps, restBeforeParent) = getParentLabelOrMatchesAndRestBefore(matchOrLabelId) restOriginalBodiesAndParentRest.getOrElseUpdate( @@ -300,12 +330,67 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo allBranchesOfDtor.keysIterator.foreach(fvsForDtor) } + // update the correct new erased type info to the new symbols before rewriting bodies that refer to them. + // we do this after information like free variables are computed, as those are needed for branch function types + locally { + for (branchId, params) <- branchFunParamFieldSyms do + val ctors = ctorWhichBranch.iterator.collect: + case (id, branch) if branch == branchId => solver.fusingCtorInfo(id) + .toList + params.zipWithIndex.foreach: (param, index) => + val types = ctors.map: ctor => + val (field, prod) = ctor.args(index) + slotTypeFromFlow(producersOf(prod), field.erasedFieldType) + param.erasedType = if types.forall(_.isEmpty) then N + else S(ErasedType.Union.mk(types.map(_.getOrElse(ErasedType.Unknown)))) + for (branchId, (_, sym)) <- branchFunSyms do + sym.erasedType = S(ErasedType.FuncRef(S(false), + branchFunParamFieldSyms(branchId).map(_.erasedType) :: + dtorBranchFnFvs(branchId._1).map(s => retypedVariables.get(branchId._1.instId -> s).orElse(s.mapErasedValueType)) + :: Nil, N)) + for (restId, (_, sym)) <- restFunSyms do + sym.erasedType = S(ErasedType.FuncRef(S(false), + restFnFvs(restId).map(s => retypedVariables.get(restId.getInstId -> s).orElse(s.mapErasedValueType)) :: Nil, N)) + } + // Rewrites the program under a specific instantiation id // from the polymorphic analysis protected class Rewriter(instId: InstantiationId, refreshSymbolMappingForCopy: Opt[collection.Map[LocalVarSymbol, LocalVarSymbol]]) extends InstantiationRewriter(instId, refreshSymbolMappingForCopy): + // the result slot of the function whose body is being rewritten + private type SlotType = (original: Opt[ErasedValueType], retyped: Opt[ErasedValueType]) + private var returnType: SlotType = (N, N) + private def returningInto[A](tpe: SlotType)(rewrite: => A): A = + val enclosing = returnType + returnType = tpe + try rewrite finally returnType = enclosing + def rewriteFunDefn(fun: FunDefn): RewrittenFunDefn = - fun.params.mapConserve(applyParamList) -> applyBlock(fun.body) + val declared = fun.dSym.declaredResultType + fun.params.mapConserve(applyParamList) -> + returningInto(declared -> retypedFunctions.get(instId -> fun.dSym).fold(declared)(_.ret)): + applyBlock(fun.body) + + override def applyFunDefn(fun: FunDefn): FunDefn = + returningInto(fun.dSym.declaredResultType -> fun.dSym.subst.declaredResultType): + super.applyFunDefn(fun) + + // the callee is a closure or an extracted function, whose results are unknown, so like any call to a + // function value, the result is cast into the slot that the replaced `return`s wrote + private def returnCall(call: Result): Block = Return(returnType.retyped.fold(call)(call.coerceTo(_, N))) + + // a slot that fusion retypes takes the type the flow gives it + // the flow analysis proves the value has the slot's type, so the cast should never fail + private def intoRetypedSlot[R <: Result](r: R, slot: SlotType)(k: (R | Path) => Block): Block = + if slot.retyped == slot.original then k(r) + else coerceIntoFlowTypedSlot(r, slot.retyped.getOrElse(ErasedType.Unknown))(k) + + // as the flow proves that a value reaching a slot typed from the flow has the slot's type, a value whose type is + // unrelated to it is never produced (it is read in a dead branch, or is the result of a call that never + // returns): its effects are kept, and `undefined` stands in for it + private def coerceIntoFlowTypedSlot[R <: Result](r: R, tpe: ErasedType)(k: (R | Path) => Block): Block = + r.coerceToOr(tpe, N)(k): (_, _) => + Assign.discard(r, k(Value.Lit(Tree.UnitLit(false))(N).coerceTo(tpe, N))) // calls `fun` with possibly remapped `args` def mkCall(fun: Path, args: Ls[LocalVarSymbol])(k: Result => Block): Block = @@ -314,6 +399,7 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo override def applyResult(r: Result)(k: Result => Block): Block = r match + case c: Cast if fusedCasts(c.uid.concreteId) => applyResult(c.value)(k) case s@TrackableSelect(from, _, _) => if branchSelSyms.isDefinedAt(s.uid.concreteId) then k(branchSelSyms(s.uid.concreteId).subst.asSimpleRef) @@ -325,39 +411,66 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo def mkCtorFieldSyms(ctorDtorId: CtorDtorId): Ls[TempSymbol] = val ctorInfo = solver.fusingCtorInfo(ctorDtorId) val clsNme = ctorInfo.ctor.ctorClsName - ctorInfo.args.unzip._1.map: f => - new TempSymbol(N, erasedType = f.erasedFieldType, s"${clsNme}_${f.fieldName}") + ctorInfo.args.map: (field, prod) => + new TempSymbol(N, erasedType = slotTypeFromFlow(producersOf(prod), field.erasedFieldType), s"${clsNme}_${field.fieldName}") end mkCtorFieldSyms + def fillFieldSyms(fieldSyms: Ls[TempSymbol], after: Block): Block = + args.zip(fieldSyms).foldRight(after): + case (Arg(N, a) -> fieldSym, rest) => + applyPath(a): fusedField => + coerceIntoFlowTypedSlot(fusedField, fieldSym.erasedType.getOrElse(ErasedType.Unknown)): value => + Scoped(Set.single(fieldSym), Assign(fieldSym, value, rest)) + case _ => TODO("spread args are not supported") + solver.finalCtorDests.get(ctor.uid.concreteId) match case None => super.applyResult(ctor)(k) case Some(FinalDestSel(_, field)) => val ctorInfo = solver.fusingCtorInfo(ctor.uid.concreteId) val idx = ctorInfo.args.unzip._1.indexOf(field) val fieldSyms = mkCtorFieldSyms(ctor.uid.concreteId) - args.zip(fieldSyms).foldRight(k(fieldSyms(idx).asSimpleRef)): - case (Arg(N, a) -> s, rest) => - applyPath(a): fusedField => - Scoped(Set.single(s), Assign(s, fusedField, rest)) - case _ => TODO("spread args are not supported") + fillFieldSyms(fieldSyms, k(fieldSyms(idx).asSimpleRef)) case Some(_: FinalDestMatch) => val fieldSyms = mkCtorFieldSyms(ctor.uid.concreteId) val (branchFunBms, branchFunTSym) = branchFunSyms(ctorWhichBranch(ctor.uid.concreteId)) - args.zip(fieldSyms).foldRight(mkCall(branchFunBms.asMemberRef(branchFunTSym), fieldSyms)(k)): - case (Arg(N, a) -> fieldSym, rest) => - applyPath(a): fusedField => - Scoped(Set.single(fieldSym), Assign(fieldSym, fusedField, rest)) - case _ => TODO("spread args are not supported") + fillFieldSyms(fieldSyms, mkCall(branchFunBms.asMemberRef(branchFunTSym), fieldSyms)(k)) + case c @ Call(FunRef(f, _), _) => super.applyResult(c): + // a copy that fusion does not retype keeps `f.erasedType` itself, even where that type is stale + case Call(fun2 @ FunRef(f2, _), argss2) if f2.erasedType != f.erasedType => + // TODO: `original` should probably be `f.erasedType`'s parameter lists, + // but the lifter does not seem to update type correctly now + val original = pre.res.funSymToFunDefn(f).params.map(_.params.map(_.sym.erasedType)) + val retyped = f2.erasedType match + case S(ft: ErasedFuncType) => ft.paramLists + case _ => Nil + val slotss = original.iterator.zip(retyped).map(_.iterator.zip(_)) + // truncating `slotss` only drops the lists a partial application leaves unapplied + val argss3 = argss2.iterator.zip(slotss).map: (args, slots) => + if args.exists(_.spread.isDefined) then args.map(_ -> N) + else args.map(_ -> slots.nextOption()) + .toList + applyListOf(argss3, applyListOf(_, (arg, k2) => arg match + case (a, S(slot)) => intoRetypedSlot(a.value, slot)(value => k2(Arg(N, value) -> N)) + case _ => k2(arg) + )(_)): argss4 => + k(Call(fun2, argss4.map(_.map(_._1)).ne_!)(c.metadata, c.toLoc)) + case c2 => k(c2) case _ => super.applyResult(r)(k) override def applyPath(p: Path)(k: Path => Block): Block = p match case PolyFnRef(specializedRef) => k(specializedRef) + case c: Cast if fusedCasts(c.uid.concreteId) => + applyResult(c.value): + case p: Path => k(p) + case r => + val tmp = TempSymbol(N, r.erasedValueType, "fused") + Scoped(Set.single(tmp), Assign(tmp, r, k(tmp.asPath))) case ctor@CtorProducer(_, args, _) if solver.finalCtorDests.isDefinedAt(ctor.uid.concreteId) => assert(args.isEmpty) val (branchFunBms, branchFunTSym) = branchFunSyms(ctorWhichBranch(ctor.uid.concreteId)) - val lambdaSym = new TempSymbol(N, erasedType = N, "deforest$lam") mkCall(branchFunBms.asMemberRef(branchFunTSym), Nil): callBranchFun => + val lambdaSym = new TempSymbol(N, erasedType = callBranchFun.erasedValueType, "deforest$lam") Scoped( Set.single(lambdaSym), Assign( @@ -386,13 +499,26 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo b match case m@Match(scrut, _, _, _) if solver.finalDtorSrcs.isDefinedAt(scrut.uid.concreteId) => applyPath(scrut): newScrut => - mkCall(newScrut, dtorBranchFnFvs(scrut.uid.concreteId))(Return(_)) + mkCall(newScrut, dtorBranchFnFvs(scrut.uid.concreteId))(returnCall) + case Assign(l: LocalVarSymbol, r, rst) => + applyResult(r): r2 => + val l2 = applyAssignLhs(l) + val slot = l2 match + case l2: LocalVarSymbol => (l.mapErasedValueType, l2.mapErasedValueType) + case NoSymbol => (N, N) + intoRetypedSlot(r2, slot): r3 => + val rst2 = applySubBlock(rst) + if (l2 is l) && (r3 is r) && (rst2 is rst) then b else Assign(l2, r3, rst2) + case Return(res) => + applyResult(res): res2 => + intoRetypedSlot(res2, returnType): res3 => + if res3 is res then b else Return(res3) case Break(label) => val labelRestFunId = label.withInstId(instId) restFunSyms.get(labelRestFunId) match case None => super.applyBlock(b) case Some((bms, tSym)) => - mkCall(bms.asMemberRef(tSym), restFnFvs(labelRestFunId))(Return(_)) + mkCall(bms.asMemberRef(tSym), restFnFvs(labelRestFunId))(returnCall) // copying a definition would need fresh symbols for it; lifting removes the nested ones beforehand case Define(defn, _) if refreshSymbolMappingForCopy.isDefined => softAssert(false, s"deforestation copying the definition ${defn.sym.nme}") @@ -418,7 +544,8 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo // the rewriter of an extracted part (branch body functions and match rest functions), // whose free variables `fvs` become the `fv_` parameters it returns def mkExtractedBodyRewriter(instId: InstantiationId, body: Block, fvs: Ls[LocalVarSymbol]): (Rewriter, Ls[VarSymbol]) = - val fvParams = fvs.map(s => new VarSymbol(Tree.Ident(s"fv_${s.nme}"), erasedType = s.mapErasedValueType)) + val fvParams = fvs.map: s => + new VarSymbol(Tree.Ident(s"fv_${s.nme}"), erasedType = retypedVariables.get(instId -> s).orElse(s.mapErasedValueType)) mkCopyRewriter(instId, Nil, body, Map.from(fvs.iterator.zip(fvParams))) -> fvParams // Functions for fused match branches; each runs one arm and then its rest @@ -466,9 +593,37 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo override def applyFunDefn(fun: FunDefn): FunDefn = rewrittenInPlace.get(fun.dSym) match case Some((params, rewrittenBody)) => - // deforest never changes fun params in place - assert(params is fun.params) - FunDefn(fun.owner, fun.sym, fun.dSym, params, rewrittenBody)(fun.configOverride, fun.annotations) + FunDefn(fun.owner, fun.sym, fun.dSym.subst, params, rewrittenBody)(fun.configOverride, fun.annotations) case None => super.applyFunDefn(fun) + + lazy val retypedVariables = + val slots = LinkedHashMap.empty[(InstantiationId, ValueSymbol), ErasedValueType] + for (sym, instId, v) <- collector.symbolFlows do sym match + case sym: (VarSymbol | TempSymbol | TermSymbol) => + retypedSlotType(v.lowerBounds.toList, sym.mapErasedValueType).foreach: + tpe => slots(instId -> sym) = tpe + case _ => () + slots + + // a function is retyped if fusion retypes one of its parameters or changes what it returns + lazy val retypedFunctions = + val res = LinkedHashMap.empty[(InstantiationId, TermSymbol), ErasedFuncType] + for pf <- constraintSolver.prodFunsWithDests do pf.exprId match + case (fun: TermSymbol, idx) if idx == pre.res.funSymToFunDefn(fun).params.size - 1 => + val instId = pf.concreteId.instId + val key = instId -> fun + var newParams = false + val paramsTypes = pre.res.funSymToFunDefn(fun).params.map: + _.params.map: p => + retypedVariables.get(instId -> p.sym) match + case N => p.sym.erasedType + case S(newTpe) => newParams ||= true; S(newTpe) + val maybeNewReturnType = retypedSlotType(producersOf(pf.res), fun.declaredResultType) + if maybeNewReturnType.isDefined || newParams then + val retType = maybeNewReturnType.orElse(fun.declaredResultType) + val funType = ErasedType.FuncRef(S(false), paramsTypes, retType) + res(key) = funType + case _ => () + res // ====== end: implements the abstract members of `PolyInstantiationRewrite` ====== end DeforestRewriter diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala index 06ea839f99..60383f629e 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala @@ -78,7 +78,7 @@ object FlowAnalysis: mono: Bool, nonAffineTracking: Bool, accumulatorTracking: Bool, - )(using TraceLogger, Elaborator.State, Raise, SymbolPrinter) = + )(using TraceLogger, Elaborator.State, Elaborator.Ctx, Raise, SymbolPrinter) = given State = new State val pre = new FlowPreAnalyzer(pgrm) val constrCol = new FlowConstraintsCollector(pre, mono, nonAffineTracking, accumulatorTracking) @@ -181,7 +181,7 @@ object FunRef: sealed trait ProdStrat sealed trait ConsStrat -class StratVar(val name: Str, val sourceSymbol: Opt[Symbol], val generatedForFun: Opt[TermSymbol])(using eState: Elaborator.State) +class StratVar(val name: Str, val sourceSymbol: Opt[Symbol], val generatedForFun: Opt[TermSymbol], val instantiationId: Opt[InstantiationId])(using eState: Elaborator.State) extends Symbol(using eState) with ProdStrat with ConsStrat: def nme: Str = name def subst(using SymbolSubst): StratVar = this @@ -200,13 +200,13 @@ object StratVar: def freshVar(nme: String)(using fState: FlowAnalysis.State): StratVar = - freshVar(nme, N, N) + freshVar(nme, N, N, N) def freshVar(nme: String, generatedForFun: TermSymbol)(using fState: FlowAnalysis.State): StratVar = - freshVar(nme, N, S(generatedForFun)) - def freshVar(nme: String, sourceSymbol: Opt[Symbol], generatedForFun: Opt[TermSymbol])(using fState: FlowAnalysis.State): StratVar = + freshVar(nme, N, S(generatedForFun), N) + def freshVar(nme: String, sourceSymbol: Opt[Symbol], generatedForFun: Opt[TermSymbol], instantiationId: Opt[InstantiationId])(using fState: FlowAnalysis.State): StratVar = val ownName = if nme.isEmpty then "$stratvar" else nme val name = generatedForFun.fold(ownName)(fun => s"${ownName}_for_${fun.nme}") - val stratVar = StratVar(name, sourceSymbol, generatedForFun)(using fState.eState) + val stratVar = StratVar(name, sourceSymbol, generatedForFun, instantiationId)(using fState.eState) fState.stratVars += stratVar stratVar def freshVar(nme: String, forFunOpt: Opt[TermSymbol])(using fState: FlowAnalysis.State): StratVar = @@ -256,6 +256,9 @@ class Ctor( val args: Ls[SelField -> ProdStrat] ) extends ProdStrat with StratWithOrigin[ResultId]: val dests = MutSet.empty[ConcreteCtorConsumer | MarkerConsStrat] + def allocatedType: ErasedValueType = ctor match + case cls: (ClassSymbol | ModuleOrObjectSymbol) => ErasedType.ValueLike(S(false), cls) + case _: Int => ErasedType.Array override def toString(): String = s"$ctor(${args.map(_.toString()).mkString(", ")})" @@ -271,6 +274,18 @@ class ConsFun( override def toString(): String = s"(${params.map(_.toString()).mkString(", ")}) -> ${res.toString()}" +/** A cast observes an object's representation only when its success cannot be proved. + * Its origin is retained through scheme copies so every producer of one physical cast + * participates in the same proof, including multiple instances of a recursive scheme. + */ +class CastConsumer( + val exprId: ResultId, + val instantiationId: Opt[InstantiationId], + val target: ErasedValueType, +) extends ConsStrat with StratWithOrigin[ResultId]: + val sources = LinkedHashSet.empty[ProdStrat] + var succeeds = true + case object UnknownCons extends MarkerConsStrat case object NonAffine extends MarkerConsStrat @@ -390,7 +405,7 @@ class FlowPreAnalyzer(val pgrm: Program)(using def registerStratVar(sym: Symbol, nme: String): Unit = val currentRootFun = ctx.tails.collectFirst: case InCtx.Fn(fun) :: tl if isTopLvlLikeFunCtx(tl) => fun.dSym - res.generatedVars.getOrElseUpdate(sym, freshVar(nme, S(sym), currentRootFun)) + res.generatedVars.getOrElseUpdate(sym, freshVar(nme, S(sym), currentRootFun, N)) private inline def withCtx(newCtx: InCtx)(inline body: => Any)(after: => Unit = ()): Unit = ctx = newCtx :: ctx @@ -682,7 +697,7 @@ class FlowConstraintsCollector( val mono: Bool, val nonAffineTracking: Bool, val accumulatorTracking: Bool, -): +)(using val eCtx: Elaborator.Ctx): given FlowPreAnalyzer = preAnalyzer given Raise = preAnalyzer.raise given fState: FlowAnalysis.State = preAnalyzer.fState @@ -722,6 +737,13 @@ class FlowConstraintsCollector( private val generatedVars: collection.Map[Symbol, StratVar] = preAnalyzer.res.generatedVars.withDefaultValue(preAnalyzer.res.primitiveStratVar) + // Keep the solved flows for each physical copy of a symbol. A representation-changing + // rewrite needs locals as well as parameters; the original scheme variables have no + // solved bounds, and variables copied through several schemes retain the full path. + val symbolFlows = mutable.Buffer.empty[(Symbol, InstantiationId, StratVar)] + for (sym, v) <- preAnalyzer.res.generatedVars if mono || v.generatedForFun.isEmpty do + symbolFlows += ((sym, Nil, v)) + locally { val funsToProdStratScheme = MutMap.empty[TermSymbol, ProdStratScheme] @@ -820,7 +842,13 @@ class FlowConstraintsCollector( def duplicateVarState(s: StratVar) = if s.generatedForFun.fold(false): forFun => funToSccRep(forFun).fold(false)(_ is groupRep) - then stratVarMap.getOrElseUpdate(s, freshVar(s.name, s.sourceSymbol, cc.forFun)) + then stratVarMap.getOrElseUpdate(s, { + val instId = updateInstantiationId(s.instantiationId) + val v = freshVar(s.name, s.sourceSymbol, cc.forFun, instId) + for sym <- s.sourceSymbol if cc.forFunGroup.isEmpty do + symbolFlows += ((sym, instId.get, v)) + v + }) else s def duplicateProdStrat(s: ProdStrat): ProdStrat = s match case v: StratVar => duplicateVarState(v) @@ -842,6 +870,7 @@ class FlowConstraintsCollector( new ConsFun(c.exprId, updateInstantiationId(c.instantiationId))( c.params.map(duplicateProdStrat), duplicateConsStrat(c.res)) + case c: CastConsumer => new CastConsumer(c.exprId, updateInstantiationId(c.instantiationId), c.target) case UnknownCons => UnknownCons case NonAffine => NonAffine case Accumulator => Accumulator @@ -1084,9 +1113,9 @@ class FlowConstraintsCollector( kind match case DynSelectKind.Member(sym) => generatedVars(sym) case DynSelectKind.ArrayIdx | DynSelectKind.Unknown => UnknownProd - case Cast(value, _, _) => + case c @ Cast(value, target, _) => val valueStrat = processResult(value) - cc.constrain(valueStrat, UnknownCons) + cc.constrain(valueStrat, new CastConsumer(c.uid, instId, target)) valueStrat case Value.MemberRef(_, disamb) => generatedVars(disamb) case Value.SimpleRef(sym) => generatedVars(sym) @@ -1096,6 +1125,7 @@ class FlowConstraintsCollector( end FlowConstraintsCollector class FlowConstraintSolver(val collector: FlowConstraintsCollector): + given Elaborator.Ctx = collector.eCtx given tl: TraceLogger = collector.tl given fState: FlowAnalysis.State = collector.fState given eState: Elaborator.State = collector.eState @@ -1108,6 +1138,7 @@ class FlowConstraintSolver(val collector: FlowConstraintsCollector): val consumersWithSrcs = mutable.Buffer.empty[ConcreteCtorConsumer] val prodFunsWithDests = mutable.Buffer.empty[ProdFun] val consFunsWithSrcs = mutable.Buffer.empty[ConsFun] + val castsWithSrcs = mutable.Buffer.empty[CastConsumer] private def addCtorDest(c: Ctor, d: ConcreteCtorConsumer | MarkerConsStrat): Unit = if c.dests.isEmpty then ctorsWithDests += c @@ -1288,6 +1319,20 @@ class FlowConstraintSolver(val collector: FlowConstraintsCollector): for l <- p.lowerBounds do handle(l, c) case (p, c: StratVar) => if c.lowerBounds.add(p) then for u <- c.upperBounds do handle(p, u) + case (p, c: CastConsumer) => + if c.sources.isEmpty then castsWithSrcs += c + if c.sources.add(p) then + val actual = p match + case ctor: Ctor => S(ctor.allocatedType) + case _: ProdFun => S(ErasedType.Function(S(false))) + case UnknownProd => N + case _: StratVar => lastWords("cast producer variable was not propagated") + val succeeds = actual.exists: tpe => + ErasedType.needsCast(tpe.canonicalize, c.target.canonicalize).contains(false) + if c.succeeds && !succeeds then + c.succeeds = false + c.sources.toList.foreach(handle(_, UnknownCons)) + else if !c.succeeds then handle(p, UnknownCons) case _ => () // ignore other cases end handle diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowWebComputation.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowWebComputation.scala index ea3a0b8abe..5201553d86 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowWebComputation.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowWebComputation.scala @@ -8,15 +8,21 @@ object FlowWebComputation: case class Result[P, C](markedProducers: collection.Set[P], markedConsumers: collection.Set[C]) def apply[P, C]( + // marking a producer marks the consumers it flows to dests: P => IterableOnce[C], + // marking a consumer marks the producers flowing into it srcs: C => IterableOnce[P], + // marking a producer also marks these, which no consumer links to it + impliedProducers: P => IterableOnce[P], producerRoots: IterableOnce[P], consumerRoots: IterableOnce[C], ): Result[P, C] = val markedProducers = MutSet.empty[P] val markedConsumers = MutSet.empty[C] def markProducer(p: P): Unit = - if markedProducers.add(p) then dests(p).iterator.foreach(markConsumer) + if markedProducers.add(p) then + dests(p).iterator.foreach(markConsumer) + impliedProducers(p).iterator.foreach(markProducer) def markConsumer(c: C): Unit = if markedConsumers.add(c) then srcs(c).iterator.foreach(markProducer) producerRoots.iterator.foreach(markProducer) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala index ec9c751fb4..e840b25122 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/Ctx.scala @@ -506,12 +506,15 @@ object FunctionCtx: * The parameters of this function. * @param thisSym * The implicit `this` parameter symbol if this function is generated from a non-static method, or `N` otherwise. + * @param resultType + * The physical result slot, used to represent undefined returns and bridge virtual overrides. * @param paramTypes * When set, overrides the loaded type of each param slot at its position (index 0 = `this`). */ class FunctionCtx( _params: Ls[LoweredParamList], thisSym: Opt[InnerSymbol], + val resultType: Opt[ValType], paramTypes: Opt[Seq[ValType]] = N, )(using Ctx, Raise, State): @@ -619,6 +622,7 @@ end FunctionCtx def genFuncBody[T]( params: Ls[LoweredParamList], thisSym: Opt[InnerSymbol], + resultType: Opt[ValType], paramTypes: Opt[Seq[ValType]] = N, )(mkBody: FunctionCtx ?=> T)(using Ctx, Raise, State): T -> FunctionCtx = // * Calls pad omitted optional arguments with null to preserve Wasm's fixed arity. @@ -630,7 +634,7 @@ def genFuncBody[T]( raise(ErrorReport( msg"Variadic parameters are not supported by the WebAssembly backend." -> p.toLoc.orElse(ps.toLoc) :: Nil, source = Diagnostic.Source.Compilation)) - val funcCtx = FunctionCtx(params, thisSym, paramTypes) + val funcCtx = FunctionCtx(params, thisSym, resultType, paramTypes) val result = mkBody(using funcCtx) result -> funcCtx diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala index e0f10ee7bd..e8ea718f7c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/wasm/text/WatBuilder.scala @@ -69,7 +69,9 @@ extension (et: ErasedType) val structSym = if tpeSym eq State.unitSymbol then S(State.unitBlockMemberSymbol) else tpeSym.asBlkMember - structSym.flatMap(ctx.getType).map(RefType(_, nullable = false)) + // Source slots also admit undefined, represented by a null reference. + // Known receivers and newly allocated instances can still use non-null references. + structSym.flatMap(ctx.getType).map(RefType(_, nullable = true)) case ErasedType.Primitive(prim) => S(prim.wasmType) case _ => N @@ -361,7 +363,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui * * Emits an error if the two types have incompatible type hierarchies. */ - private def castConserve(expr: Expr, target: ValType)(using Raise): Expr = + private def castConserve(expr: Expr, target: ValType, loc: Opt[Loc])(using Raise): Expr = require(expr.resultTypes.size == 1, "expected single-result expression for cast") expr.resultType -> target match case (S(ty), _) if ty.isSubtypeOf(target) => expr @@ -371,25 +373,33 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui errExpr( Ls(msg"Cannot cast a Wasm value of type `${ ty.fold("(none)")(_.toWat.mkString()) - }` to `${target.toWat.mkString()}` in a different type hierarchy" -> N), + }` to `${target.toWat.mkString()}` in a different type hierarchy" -> loc), extraInfo = S(expr.toWat.mkString()), ) /** Compiles `r` and narrows it to `target`, the declared type of the slot it flows into. */ private def castToValType(r: codegen.Result, target: ValType)(using FunctionCtx, Raise, SessionExportCtx): Expr = - castConserve(result(r), target) - - /** Supply every physical parameter, including null for omitted optional arguments, and cast - * supplied values to the callee's signature. Source arity checking happens before parameter - * flags are erased; a missing slot must still have a representation that can carry null. */ - private def adaptCallArgs(wasmArgs: Seq[Expr], funcTypeInfo: TypeInfo, loc: Opt[Loc])(using Raise): Seq[Expr] = + // Undefined supplies no value to convert: construct an unspecified value directly in the + // destination representation. This also covers placeholders introduced after lowering by DPE. + if r.isUndefined then target.zeroValue.getOrElse(lastWords("Undefined requires a defaultable value type")) + else castConserve(result(r), target, r.toLoc) + + /** Compile arguments in their physical parameter representations, including undefined placeholders, + * and supply null for omitted optional arguments. A method's already-evaluated receiver occupies + * the first parameter. Source arity checking happens before parameter flags are erased; a missing + * slot must still have a representation that can carry null. + */ + private def adaptCallArgs(args: Seq[Arg], funcTypeInfo: TypeInfo, receiver: Opt[Expr], loc: Opt[Loc]) + (using FunctionCtx, Raise, SessionExportCtx): Seq[Expr] = val declParams = funcTypeInfo.asFunctionType_!.sigType.params - if wasmArgs.size > declParams.size then raise(ErrorReport( + val prefix = receiver.map(castConserve(_, declParams.head.valtype, loc)).toSeq + val suppliedParams = declParams.drop(prefix.size) + if args.size > suppliedParams.size then raise(ErrorReport( msg"More arguments were supplied than the WebAssembly call supports." -> loc :: Nil, source = Diagnostic.Source.Compilation)) - declParams.zipWithIndex.map: (p, index) => - wasmArgs.lift(index) match - case S(arg) => castConserve(arg, p.valtype) + prefix ++ suppliedParams.zipWithIndex.map: (p, index) => + args.lift(index) match + case S(arg) => argument(arg, S(p.valtype)) case N => p.valtype match case rt: RefType if rt.nullable => ref.`null`(rt.heapType) case _ => errExpr(Ls(msg"Missing argument cannot be represented as null by the WebAssembly backend." -> loc)) @@ -932,11 +942,12 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui exportName: Opt[Str], funcTy: TypeIdx, )(using Raise): Unit = + val signature = ctx.getTypeInfo_!(funcTy).asFunctionType_!.sigType ctx.addFunc(FuncInfo( sym, wrapId = if sym.asClsOrMod.isDefined then (N -> S("ctor")) else (S(defn.sym.nme) -> N), typeUse = TypeUse(funcTy), - params = params.map(_.resolvedParam), + params = params.zip(signature.params).map { case ((symbol, id), param) => symbol -> WasmParam(id, param.valtype) }, resultTypes = resultTypes, locals = Seq.empty, body = ref.`null`(ctx.getType_!(defn.sym)), @@ -1402,6 +1413,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui genFuncBody( classCtorParamList(clsLikeDefn) :: Nil, thisSym = S(clsLikeDefn.isym), + resultType = N, paramTypes = S(Seq(RefType(ctx.getType_!(clsLikeDefn.sym), nullable = false))), ): val thisVar = funcCtx.lookupLocal_!(clsLikeDefn.isym, N) @@ -1448,11 +1460,11 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui case S(parentSym) => val parentTypeRef = ctx.getType_!(parentSym) val parentInitFunc = initFuncSym(parentSym) - val parentFuncType = ctx.getTypeInfo_!(ctx.getFuncTypeUse_!(parentInitFunc).typeIdx) + val parentInitType = ctx.getTypeInfo_!(ctx.getFuncTypeUse_!(parentInitFunc).typeIdx) val superCall = call( funcidx = ctx.getFunc_!(parentInitFunc), - operands = adaptCallArgs(local.get(thisVar, RefType(parentTypeRef, nullable = false)) +: - args.map(argument), parentFuncType, clsLikeDefn.sym.toLoc), + operands = adaptCallArgs(args, parentInitType, + S(local.get(thisVar, RefType(parentTypeRef, nullable = false))), clsLikeDefn.sym.toLoc), returnTypes = Seq(Result(RefType(parentTypeRef, nullable = false))), ) prefixWat :+ drop(superCall) @@ -1597,6 +1609,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui castConserve( global.get(globalIdx, ctx.getGlobalType_!(globalIdx).globalType.valType), l.localType, + loc, ) case N => errExpr( @@ -1631,13 +1644,13 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui ), ) - def argument(a: Arg)(using FunctionCtx, Raise, SessionExportCtx): Expr = + private def argument(a: Arg, expected: Opt[ValType])(using FunctionCtx, Raise, SessionExportCtx): Expr = if a.spread.nonEmpty then errExpr( Ls(msg"WatBackend::argument for spread expression not implemented yet" -> a.value.toLoc), extraInfo = S(a.showAsTree), ) - else result(a.value) + else expected.fold(result(a.value))(castToValType(a.value, _)) def operand(a: Arg)(using FunctionCtx, Raise, SessionExportCtx): Expr = if a.spread.nonEmpty then die else subexpression(a.value) @@ -1707,7 +1720,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui ownerTypeInfoRef, RefType(virtualMethodTypeIdx, nullable = true), ) - val operands = adaptCallArgs(receiverRef +: args.map(argument), ctx.getTypeInfo_!(virtualMethodTypeIdx), qual.toLoc) + val operands = adaptCallArgs(args, ctx.getTypeInfo_!(virtualMethodTypeIdx), S(receiverRef), qual.toLoc) val virtualCall = call_ref( target = methodRef, operands = operands, @@ -1717,11 +1730,11 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui // The slot carries the *base* declaration's result type, but the resolved method's declared result may be // overridden to be narrower. Restore the slot's result value to the resolved method's declared type here - // the checked cast ensures that a value that doesn't conform will trap at runtime. - val adaptedCall = castConserve(virtualCall, declaredResultType(methodSym)) + val adaptedCall = castConserve(virtualCall, declaredResultType(methodSym), N) Ls(receiverExpr, adaptedCall) case N => val funcTypeInfo = ctx.getTypeInfo_!(ctx.getFuncTypeUse_!(methodSym).typeIdx) - val operands = adaptCallArgs(result(qual) +: args.map(argument), funcTypeInfo, qual.toLoc) + val operands = adaptCallArgs(args, funcTypeInfo, S(result(qual)), qual.toLoc) Ls(call( funcidx = ctx.getFunc_!(methodSym), operands = operands, @@ -1777,6 +1790,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui sym.asBlkMember.fold(baseObjectTypeIdx)(ctx.getType_!(_)), nullable = false, ), + r.toLoc, ) case Call(Value.SimpleRef(l: BuiltinSymbol), lhs :: rhs :: Nil) if !l.functionLike => @@ -1862,7 +1876,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui case S(_) => call( funcidx = getIntrinsic(intrName), - operands = args.map(argument), + operands = args.map(argument(_, N)), returnTypes = Seq(Result(RefType.anyref)), ) case N => @@ -1876,7 +1890,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui extraInfo = S(fun.toString), ) val baseTypeInfo = ctx.getTypeInfo_!(ctx.getFuncTypeUse_!(baseFuncIdx).typeIdx) - val wasmArgs = adaptCallArgs(args.map(argument), baseTypeInfo, c.toLoc) + val wasmArgs = adaptCallArgs(args, baseTypeInfo, N, c.toLoc) call( funcidx = baseFuncIdx, @@ -1903,7 +1917,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui ), ) val baseTypeInfo = ctx.getTypeInfo_!(baseTypeIdx) - val wasmArgs = adaptCallArgs(args.map(argument), baseTypeInfo, c.toLoc) + val wasmArgs = adaptCallArgs(args, baseTypeInfo, N, c.toLoc) call_ref( target = base, @@ -1956,14 +1970,14 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui case other => lastWords(s"Expected struct type for $selCls, found ${other.toWat.mkString()}") val getExpr = struct.get( fieldidx, - ref = castConserve(qualRes, RefType(ctx.getType_!(selCls), nullable = false)), + ref = castConserve(qualRes, RefType(ctx.getType_!(selCls), nullable = false), sel.toLoc), ty = fieldTy, ) selSym.asTrm.flatMap(_.erasedType).flatMap(_.wasmType) match - case S(declared) => castConserve(getExpr, declared) + case S(declared) => castConserve(getExpr, declared, sel.toLoc) case N => fieldTy match case rt: RefType if rt.nullable && rt.heapType != HeapType.Any => - castConserve(getExpr, rt.copy(nullable = false)) + castConserve(getExpr, rt.copy(nullable = false), sel.toLoc) case _ => getExpr case N => errExpr( @@ -2047,11 +2061,11 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui lastWords(s"Missing type definition for class constructor ${ctorClsBlkSym.toString}") val ctorClsTypeIdx = ctx.getType(ctorClsBlkSym).getOrElse: lastWords(s"Missing class definition for class ${ctorClsBlkSym.toString}") - val ctorArgs = adaptCallArgs(as.map(argument), ctorTypeIdx, r.toLoc) + val ctorArgs = adaptCallArgs(as, ctorTypeIdx, N, r.toLoc) call(funcidx = ctorFuncIdx, operands = ctorArgs, Seq(Result(RefType(ctorClsTypeIdx, nullable = false)))) case Tuple(mut, elems) => - val tupleValues = elems.map(argument) + val tupleValues = elems.map(argument(_, N)) array.new_fixed(tupleArrayType(mut), tupleValues) case Cast(value, target, _) => @@ -2096,7 +2110,9 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui arg: Arg, )(using Ctx, FunctionCtx, Raise, SessionExportCtx): IntrinsicArg = def compileOperand: (Expr, Opt[Str]) = - val operand = argument(arg) + val operand = + if arg.value.isUndefined then argument(arg, declared.flatMap(_.wasmType)) + else argument(arg, N) operand -> declared .filter(_.wasmType.exists(expected => !operand.resultType.exists(_.isSubtypeOf(expected)))) .map(_.describe) @@ -2179,13 +2195,13 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui })", ) val fieldidx = fieldSelect(selCls, fieldSym) - val objRef = castConserve(lhsExpr, RefType(ctx.getType_!(selCls), nullable = false)) + val objRef = castConserve(lhsExpr, RefType(ctx.getType_!(selCls), nullable = false), lhs.toLoc) val structInfo = ctx.getTypeInfo_!(selCls) val fieldType = structInfo.compType match case st: StructType => classField(selCls, fieldSym).ty case other => lastWords(s"Expected struct type for $selCls, found ${other.toWat.mkString()}") val rhsCasted = fieldType match - case rt: RefType => castConserve(rhsExpr, rt) + case rt: RefType => castConserve(rhsExpr, rt, rhs.toLoc) case _ => rhsExpr struct.set(fieldidx, objRef, rhsCasted) case N => @@ -2293,8 +2309,8 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui // Nested functions are not predeclared in `program` - declare them now. // Note that predeclaring functions twice causes an orphaned type to be duplicated in the module. if ctx.getFunc(sym).isEmpty then predeclareTopLevelFun(sym, ps) - val (bodyWat, fnCtx) = setupFunction(N, ps, result) val predeclared = ctx.getFuncInfo_!(sym) + val (bodyWat, fnCtx) = setupFunction(N, ps, result, resultType = predeclared.resultTypes.headOption.map(_.valtype)) val funcInfo = FuncInfo( sym, @@ -2366,7 +2382,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui lastWords(s"Expected struct type for ${clsLikeDefn.sym}, found ${other.toWat.mkString()}") val initFuncRef = initFuncSym(clsLikeDefn.sym) - val (ctorCode, ctorFnCtx) = genFuncBody(ctorParamList :: Nil, thisSym = N): + val (ctorCode, ctorFnCtx) = genFuncBody(ctorParamList :: Nil, thisSym = N, resultType = N): val thisVar = bindCtorThis(clsLikeDefn.isym) val initCall = call( funcidx = ctx.getFunc_!(initFuncRef), @@ -2411,11 +2427,11 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui ps: LoweredParamList, bod: Block, ): Unit = - val paramTypes = ctx.getVirtualTable(clsLikeDefn.sym).flatMap: vt => - vt.slotOf.get(sym).map: slot => - RefType(ctx.getType_!(vt.slots(slot).owner), nullable = false) +: vt.slots(slot).paramTypes - val (bodyWat, fnCtx) = setupFunction(S(clsLikeDefn.isym), ps, bod, paramTypes = paramTypes) val predeclaredMethod = ctx.getFuncInfo_!(sym) + // Reuse the physical signature, including a non-null receiver and any inherited virtual slot. + val paramTypes = S(predeclaredMethod.getSignatureType.params.map(_.valtype)) + val (bodyWat, fnCtx) = setupFunction(S(clsLikeDefn.isym), ps, bod, + resultType = predeclaredMethod.resultTypes.headOption.map(_.valtype), paramTypes = paramTypes) val funcInfo = FuncInfo( sym, wrapId = S(clsLikeDefn.sym.nme) -> N, @@ -2492,10 +2508,14 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui end match case Return(res) => - val operand = result(res) - // Storage and calling conventions may widen a reference (e.g. a singleton's nullable global). - // Restore the IR value's type at the return boundary so Block.returnType also describes the emitted value. - val resWat = res.erasedValueType.flatMap(_.wasmType).fold(operand)(castConserve(operand, _)) + // Use the physical result slot for every return, including optimizer-generated undefined. + // Overrides share the introducing method's slot, which can be narrower than their own + // inferred reference result. Primitive mismatches remain errors, not conversions. + val target = funcCtx.resultType.orElse(res.erasedValueType.flatMap(_.wasmType)) + val resWat = + if res.isUndefined then target.fold(result(res))(castToValType(res, _)) + else result(res) + def coerced = target.fold(resWat)(castConserve(resWat, _, res.toLoc)) val returned = resWat.resultType match case S(refTy: RefType) => refTy.heapType match @@ -2503,8 +2523,8 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui errExpr(Ls(msg"Returning function instances is not supported" -> res.toLoc)) case typeidx: TypeIdx if ctx.getTypeInfo_!(typeidx).compType.isInstanceOf[FunctionType] => errExpr(Ls(msg"Returning function instances is not supported" -> res.toLoc)) - case _ => resWat - case _ => resWat + case _ => coerced + case _ => coerced Vector(`return`(S(returned))) @@ -2889,18 +2909,22 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui // Compile the entry function under a dedicated local scope so that any temp locals introduced // during codegen (e.g., via `local.tee`) are declared in the entry function. - val (entryBody, entryFnCtx) = genFuncBody(Nil, thisSym = N): + val entryType = ctx.elabCtx.givenIn(Block.returnType(p.main).map(_.canonicalize)) + val entrySlot = entryType.flatMap(_.wasmType) + val (entryBody, entryFnCtx) = genFuncBody(Nil, thisSym = N, resultType = entrySlot): normalizeValueExprs(block(p.main), p.main.isAbortive).mergeAsBlock.getOrElse(nop) val entrySym = BlockMemberSymbol("entry", Nil) + // Body generation can register synthetic types such as Unit on first use. + val entryResultType = entryType.flatMap(_.wasmType).getOrElse(RefType.anyref) - val (entryFnExpr, entryResultType) = ctx.elabCtx.givenIn: - Block.returnType(p.main).map(_.canonicalize) match + val entryFnExpr = ctx.elabCtx.givenIn: + entryType match case S(ErasedType.Incompatible(lhs, rhs)) => errExpr(Ls(msg"Wasm entry function has incompatible result types `${lhs.describe}` and `${rhs.describe}`" -> N), - extraInfo = S(entryBody.toWat.mkString())) -> RefType.anyref + extraInfo = S(entryBody.toWat.mkString())) // A block with no returns places no constraint on the REPL's single-result signature. - case ty => entryBody -> ty.flatMap(_.wasmType).getOrElse(RefType.anyref) + case _ => entryBody val entryResultTypes = Seq(Result(entryResultType)) val entryFnTy = ctx.addType(TypeInfo( @@ -2978,9 +3002,10 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui thisSym: Opt[InnerSymbol], params: LoweredParamList, body: Block, + resultType: Opt[ValType], paramTypes: Opt[Seq[ValType]] = N, )(using Raise, SessionExportCtx): (Expr, FunctionCtx) = - genFuncBody(params :: Nil, thisSym = thisSym, paramTypes = paramTypes): + genFuncBody(params :: Nil, thisSym = thisSym, resultType = resultType, paramTypes = paramTypes): block(body).mergeAsBlock.getOrElse(nop) end WatBuilder diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/BuiltinOperatorResults.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/BuiltinOperatorResults.scala new file mode 100644 index 0000000000..25d08f4b87 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/BuiltinOperatorResults.scala @@ -0,0 +1,82 @@ +package hkmc2 +package semantics + +import hkmc2.utils.*, shorthands.* + +/** WORKAROUND: the builtin operators are not declared functions, and several of them are overloaded + * (for example, `+` adds numbers and concatenates strings). Until they are declared as properly + * overloaded functions, new resolution and IR erasure approximate the result of an operator + * application from the primitive classes of its operands, using the rules below. Both use the same + * rules, so that the representation inferred for a slot an application flows into agrees with the + * erased type of that application in the IR. Without them, an application has no shapes at all, and + * a slot it flows into would only be inferred from its other inputs. + * + * The rules follow JavaScript semantics: resolution applies them only when targeting JavaScript, + * since the Wasm backend implements operators with dynamically typed runtime intrinsics. + * Remove this object, together with its uses in `NewResolver.builtinApplication` and + * `ErasedType.builtinOperatorResult`, once the builtins are overloaded declarations. + */ +object BuiltinOperatorResults: + + /** The builtin classes the rules distinguish. `Int` is a subclass of `Num`. */ + enum PrimitiveClass: + case Int, Num, Str, Bool, BigInt + + /** One alternative among the values an application can produce. */ + enum Alternative: + /** Values of a primitive class. */ + case Of(primitive: PrimitiveClass) + /** The values of the operand at this index. */ + case Operand(index: Int) + + import PrimitiveClass.*, Alternative.* + + private val comparisons = Set("==", "!=", "<", "<=", ">", ">=", "===", "!==") + private val arithmetic = Set("+", "-", "*", "/", "%") + + /** Whether some operands make the rules approximate an application of `op` to `arity` operands. */ + def approximates(op: Str, arity: Int): Bool = arity match + case 2 => comparisons(op) || arithmetic(op) || op === "," || op === "&&" || op === "||" + case 1 => op === "!" || op === "typeof" || op === "-" || op === "+" || op === "~" + case _ => false + + /** Alternatives whose union contains every value an application of the operator named `op` can produce, + * given the primitive class of each operand (`N` when it is unknown or not one of the `PrimitiveClass` cases), + * or `N` when no approximation is known. An application that throws produces no value, so it needs no + * alternative. + */ + def apply(op: Str, operands: Ls[Opt[PrimitiveClass]]): Opt[Ls[Alternative]] = + def numeric(operand: Opt[PrimitiveClass]): Bool = operand match + case S(Int | Num) => true + case _ => false + operands match + case lhs :: rhs :: Nil => op match + case op if comparisons(op) => S(Of(Bool) :: Nil) + // The comma operator evaluates both operands and yields the second. + case "," => S(Operand(1) :: Nil) + // Lowering short-circuits these operators: they yield their second operand or a Boolean. + case "&&" | "||" => S(Of(Bool) :: Operand(1) :: Nil) + // JavaScript concatenates as soon as either operand is a string. + case "+" if lhs.contains(Str) || rhs.contains(Str) => S(Of(Str) :: Nil) + // Division and remainder can produce fractions or NaN, so only these preserve Int. Like the operator + // signatures of InvalML's prelude, this ignores that results beyond the largest finite number overflow to + // Infinity, which is not an Int: checked casts of these results to Int are elided. + case "+" | "-" | "*" if lhs.contains(Int) && rhs.contains(Int) => S(Of(Int) :: Nil) + case op if arithmetic(op) && numeric(lhs) && numeric(rhs) => S(Of(Num) :: Nil) + // Mixing BigInt and Num operands throws. + case op if arithmetic(op) && lhs.contains(BigInt) && rhs.contains(BigInt) => S(Of(BigInt) :: Nil) + case _ => N + case operand :: Nil => op match + case "!" => S(Of(Bool) :: Nil) + case "typeof" => S(Of(Str) :: Nil) + case "-" | "+" if operand.contains(Int) => S(Of(Int) :: Nil) + case "-" if operand.contains(Num) => S(Of(Num) :: Nil) + case "-" if operand.contains(BigInt) => S(Of(BigInt) :: Nil) + // Unary plus converts any operand to a number, and throws for a BigInt. + case "+" => S(Of(Num) :: Nil) + // Bitwise negation converts any operand other than a BigInt to a 32-bit integer. + case "~" if operand.exists(_ isnt BigInt) => S(Of(Int) :: Nil) + case _ => N + case _ => N + +end BuiltinOperatorResults diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 98e1c6dbf0..42fc6fa1b4 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -314,6 +314,7 @@ object Elaborator: val BigInt = assumeBuiltinCls("BigInt") val Function = assumeBuiltinCls("Function") lazy val Promise = assumeBuiltinCls("Promise") + lazy val Iterator = assumeBuiltinCls("Iterator") lazy val Awaited = assumeBuiltinTpe("Awaited") lazy val Nothing = assumeBuiltinTpe("Nothing") val Error = assumeBuiltinCls("Error") @@ -1172,7 +1173,7 @@ extends Importer: private def branch(using Ctx): Cfg[PartialFunction[Tree, (Ctx, SimpleSplit)]] = // Interleaved-`let` bindings like `{ x is A then 0; let x = 1; ... }`. case LetLike(Keywrd(`let`), ident: Ident, S(rhsTree), N) => - val symbol = VarSymbol(ident, erasedType = N) + val symbol = VarSymbol(ident) val head = mkSplitLet(symbol, term(rhsTree, Trm)) ((ctx + (ident.name -> symbol)), head ~: End) // Interleaved-`do` statements like `{ x is A then 0; do log(1); ... }`. @@ -1377,8 +1378,18 @@ extends Importer: */ private def recordFieldScope(field: RcdField)(using Ctx): RcdField = rctx.state.lexicalTypeBinders(field.tsym) = ctx.typeBinders + if newResolution then field.tsym.defn.foreach(_.sign.foreach(registerSignature)) val key = field.key.orElse(symbolKey(field.field, required = false)) - field.copy()(key) + field.copy()(field.mut, key) + + /** Type arguments and mutable record fields share the same input/output interval syntax. */ + private def typeArgument(tree: Tree)(using Ctx, UnderCtx): Term = tree match + case Bra(Round, inner) => typeArgument(inner) + case Modified(kw @ Keywrd(Keyword.`in`), arg) => Term.WildcardTy(S(subterm(arg, Tpe)), N).mkLocWith(kw) + case Modified(kw @ Keywrd(Keyword.`out`), arg) => Term.WildcardTy(N, S(subterm(arg, Tpe))).mkLocWith(kw) + case Tup(Modified(in @ Keywrd(Keyword.`in`), arg1) :: Modified(out @ Keywrd(Keyword.`out`), arg2) :: Nil) => + Term.WildcardTy(S(subterm(arg1, Tpe)), S(subterm(arg2, Tpe))).mkLocWith(in, out) + case arg => subterm(arg, Tpe) /** An async expression wraps the values of `target` in a promise, whose payload * reads the binders in scope where the expression is written (see @@ -1390,7 +1401,10 @@ extends Importer: private def app(lt: Term, rt: Term)(tree: Tree.App, typ: Opt[typing.Type], resSym: FlowSymbol)(using Ctx): Term = val res = withTypeBinders(new Term.App(lt, rt)(tree, typ, resSym)) - if newResolution then listenTerm(lt)(shape => appShape(shape, rt, res)) + if newResolution then lt match + // Builtin operators have no shapes of their own; see NewResolver.builtinApplication. + case Term.Ref(op: BuiltinSymbol) => builtinApplication(op, rt, res) + case _ => listenTerm(lt)(shape => appShape(shape, rt, res)) res /** With the new underscore syntax, some operator operands delimit their own implicit lambdas: @@ -1436,7 +1450,7 @@ extends Importer: case _ => N withAppendedArg(lhs).getOrElse(App(lhs, Tup(arg :: Nil))) - private def assignment(lhs: Term, rhs: Term): Term.Assgn = + private def assignment(lhs: Term, rhs: Term)(using Ctx): Term.Assgn = // Resolve the term interpretation of the l-value before lowering. The // selected definition is recorded even when its value publishes no shape. // Use this for synthesized assignments as well as source-level `set`. @@ -1444,6 +1458,7 @@ extends Importer: listenTerm(lhs)(_ => ()) assignArrayElement(lhs, rhs) assignVariable(lhs, rhs) + assignField(lhs, rhs, ctx.scope) Term.Assgn(lhs, rhs) def ifLike(kw: Keyword.SplitLike, form: IfLikeForm, split: SimpleSplit, loc: Opt[Loc]): Term.IfLike = @@ -1451,7 +1466,8 @@ extends Importer: if newResolution then // Imperative branches are evaluated for effects; lowering discards their // values and returns unit, including when a loop executes zero iterations. - if form.isImperative then pipeTerm(unit, res) + if form.isImperative then + if completesNormally(res) then pipeTerm(unit, res) else split.results.foreach: trm => pipeTerm(trm, res) res @@ -1708,13 +1724,7 @@ extends Importer: val baseInterp = interp match case Trm | Receiver | Specialization => Specialization case _ => interp - val result = withTypeBinders(Term.TyApp(subterm(lhs, baseInterp), targs.map { - case Modified(kw @ Keywrd(Keyword.`in`), arg) => Term.WildcardTy(S(subterm(arg, Tpe)), N).mkLocWith(kw) - case Modified(kw @ Keywrd(Keyword.`out`), arg) => Term.WildcardTy(N, S(subterm(arg, Tpe))).mkLocWith(kw) - case Tup(Modified(in @ Keywrd(Keyword.`in`), arg1) :: Modified(out @ Keywrd(Keyword.`out`), arg2) :: Nil) => - Term.WildcardTy(S(subterm(arg1, Tpe)), S(subterm(arg2, Tpe))).mkLocWith(in, out) - case arg => subterm(arg, Tpe) - })(N).withLocOf(tree)) + val result = withTypeBinders(Term.TyApp(subterm(lhs, baseInterp), targs.map(typeArgument))(N).withLocOf(tree)) // A term specialization must validate its arguments even when unused. // Type and constructor interpretations have their own argument checks. if newResolution && interp == Trm then listenTerm(result)(_ => ()) @@ -2024,7 +2034,7 @@ extends Importer: case Quoted(body) => Term.Quoted(subterm(body)) case Unquoted(body) => Term.Unquoted(subterm(body)) case tree @ Case(kw, _) => - val scrut = VarSymbol(Ident("caseScrut"), erasedType = N) + val scrut = VarSymbol(Ident("caseScrut")) val body = caseSplit(scrut, tree) val params = Param.simple(scrut) :: Nil withTypeBinders(Term.Lam(PlainParamList(params)(N), body)).mkLocWith(kw) @@ -2178,7 +2188,7 @@ extends Importer: // * so the parameter and the reference are both located at the `_`, // * which lets diagnostics about either point to it. val id = new Ident("_" + unds.size).withLocOf(und) - val sym = VarSymbol(id, erasedType = N) + val sym = VarSymbol(id) unds += sym syntheticLocalRef(sym).withLocOf(und) case Annotated(lhs, rhs) => @@ -2469,8 +2479,12 @@ extends Importer: case Spread(Keywrd(Keyword.`...`), S(body)) :: sts => reportUnusedAnnotations go(sts, Nil, RcdSpread(term(body, fieldInterpretation)) :: acc)(N) - case InfixApp(lhs, Keywrd(Keyword.`:`), rhs) :: sts => + case InfixApp(key, Keywrd(Keyword.`:`), rhs) :: sts => var newCtx = ctx + // A field written `mut f: ...` can be assigned (see RcdField). + val (mutField, lhs) = key match + case Modified(Keywrd(Keyword.`mut`), lhs) => (true, lhs) + case lhs => (false, lhs) val (rlhs, rhs_t) = rhs match case _: Under => (lhs, subterm(rhs, fieldInterpretation)) case _ => @@ -2478,19 +2492,18 @@ extends Importer: case Apps(base, tups) => val rrhs = tups.foldRight(rhs): InfixApp(_, Keywrd(Keyword.`=>`), _) - (base, term(rrhs, fieldInterpretation)) + (base, if fieldInterpretation == Tpe then typeArgument(rrhs) else term(rrhs, fieldInterpretation)) val newAcc = rlhs match case id: Ident if fieldInterpretation == Tpe => // Record type fields declare interfaces, not local value bindings. // Keep their types directly so lookup never follows initializer flow. reportUnused(annotations.filterNot(_.isInstanceOf[Annot.JsSymbol])) val field = Term.Lit(StrLit(id.name)).withLocOf(id) - recordFieldScope(MemberKey.symbol(annotations).fold(RcdField.signature(field, rhs_t))( - key => RcdField.signature(field, rhs_t, key))) :: acc + recordFieldScope(RcdField.signature(field, rhs_t, mutField, MemberKey.symbol(annotations))) :: acc case id: Ident => - val sym = new VarSymbol(id, erasedType = N) + val sym = new VarSymbol(id) newCtx += id.name -> sym - recordFieldScope(RcdField(Term.Lit(StrLit(id.name)).withLocOf(id), sym.ref(id))) + recordFieldScope(RcdField.source(Term.Lit(StrLit(id.name)).withLocOf(id), sym.ref(id), mutField)) :: defineVar(sym, rhs_t)(Loc(rlhs, rhs)) :: LetDecl(sym, annotations)(id.toLoc) :: acc @@ -2498,17 +2511,17 @@ extends Importer: reportUnusedAnnotations val field = Term.Lit(lit).withLocOf(lit) recordFieldScope(if fieldInterpretation == Tpe - then RcdField.signature(field, rhs_t) - else RcdField(field, rhs_t) + then RcdField.signature(field, rhs_t, mutField, N) + else RcdField.source(field, rhs_t, mutField) ) :: acc case Bra(Round, inner) => reportUnusedAnnotations val key = term(inner, Trm) - recordFieldScope(if fieldInterpretation == Tpe then RcdField.signature(key, rhs_t) - else RcdField(key, rhs_t)) :: acc + recordFieldScope(if fieldInterpretation == Tpe then RcdField.signature(key, rhs_t, mutField, N) + else RcdField.source(key, rhs_t, mutField)) :: acc case _ => raise(ErrorReport(msg"Unexpected record key shape." -> rlhs.toLoc :: Nil)) - recordFieldScope(RcdField(Term.Error().withLocOf(rlhs), rhs_t)) :: acc + recordFieldScope(RcdField.source(Term.Error().withLocOf(rlhs), rhs_t, mutField)) :: acc newCtx.givenIn: go(sts, Nil, newAcc)(N) case (hd @ LetLike(kw @ Keywrd(`let`), Apps(id: Ident, tups), rhso, N)) :: sts @@ -3127,8 +3140,8 @@ extends Importer: case N => N def fieldOrVarSym(k: TermDefKind, id: Ident)(using Ctx): TermSymbol | VarSymbol = - if ctx.outer.inner.isDefined then TermSymbol(k, ctx.outer.inner, id, erasedType = N) - else VarSymbol(id, erasedType = N) + if ctx.outer.inner.isDefined then TermSymbol(k, ctx.outer.inner, id) + else VarSymbol(id) def param(t: Tree, inUsing: Bool, inDataClass: Bool): Ctxl[Diagnostic \/ (Param, Opt[SpreadKind], Ls[Str])] = t.desugared.asParam(inUsing).map: @@ -3545,6 +3558,7 @@ extends Importer: val (res, newCtx) = block(sts, hasResult = false, resultInterp = Trm) // TODO handle name clashes if newResolution then + prepareErasure(res) InterfaceExposure(this).check(exports, Nil) rctx.completeBlock(res) (res, newCtx) @@ -3553,6 +3567,7 @@ extends Importer: given UnderCtx = UnderCtx.none val (res, ctx) = block(sts, hasResult = false, resultInterp = Trm) if newResolution then + prepareErasure(res) if config.language.strictResolution then val exports = res.stats.collect: case d: Definition if InterfaceExposure.isPublic(d) => d.bsym diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ErasureInputs.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ErasureInputs.scala new file mode 100644 index 0000000000..617e685c9d --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ErasureInputs.scala @@ -0,0 +1,118 @@ +package hkmc2 +package semantics + +import scala.collection.mutable +import hkmc2.utils.*, shorthands.* +import Term.* + +/** Source declarations needed both when observing inference and when assigning representations. + * Several declarations are outside subStatements: split bindings, handler parameters, + * auxiliary constructor parameters, and the definitions synthesized for record fields. + * Keep their enumeration shared so every delayed slot is prepared before erasure reads it. + * + * Variables include the temporaries that elaboration declares with `LetDecl` (currently only the `old` copy + * that a backtracking assignment `set x = v in body` saves and restores). They are slots like source + * variables: a temporary without a representation would need a conversion from and back to the saved + * variable's representation, which is a checked cast, or an error for an unboxed primitive on Wasm. + */ +private[hkmc2] final class ErasureDeclarations(root: Statement): + val statements = mutable.ArrayBuffer.empty[Statement] + val parameters = mutable.LinkedHashMap.empty[VarSymbol, Param] + val variables = mutable.LinkedHashSet.empty[VarSymbol | TermSymbol | TempSymbol] + private val visited = mutable.Set.empty[Identity[Statement]] + + private def parameter(p: Param): Unit = + parameters(p.sym) = p + p.subTerms.foreach(visit) + + private def variable(sym: VarSymbol | TermSymbol | TempSymbol): Unit = + variables += sym + val declaration = sym match + case sym: (VarSymbol | TempSymbol) => sym.decl + case sym: TermSymbol => sym.decl + declaration match + case S(p: Param) => parameter(p) + case _ => () + + private def pattern(pat: Pattern): Unit = + pat.variables.symbols.foreach(variable) + pat.subTerms.foreach(visit) + + private def split(branches: SimpleSplit): Unit = branches match + case SimpleSplit.Cons(SimpleSplit.Head.Let(sym: VarSymbol, _), tail) => + variable(sym) + split(tail) + case SimpleSplit.Cons(SimpleSplit.Head.Match(_, pat, consequent), tail) => + pattern(pat) + split(consequent) + split(tail) + case SimpleSplit.Cons(_, tail) => split(tail) + case _ => () + + private def visit(statement: Statement): Unit = if visited.add(Identity(statement)) then + statements += statement + statement match + case td: TermDefinition => + td.params.foreach(_.foreach(parameter)) + td.tparams.foreach(_.foreach(parameter)) + case cls: ClassLikeDef => + cls.paramsOpt.foreach(_.foreach(parameter)) + cls.auxParams.foreach(_.foreach(parameter)) + cls.ext.foreach(visit) + cls match + case pat: PatternDef => + pat.parameters.foreach(parameter) + pattern(pat.pattern) + case _ => () + case Lam(ps, _) => ps.foreach(parameter) + case Forall(params, _, _) => params.foreach: param => + param.lb.foreach(visit) + param.ub.foreach(visit) + case Handle(_, _, _, _, definitions, _) => definitions.foreach: d => + variable(d.resumeSym) + visit(d.td) + case LetDecl(sym: (VarSymbol | TermSymbol | TempSymbol), _) => variable(sym) + case Ref(sym: VarSymbol) => variable(sym) + case SimpleRef(sym: VarSymbol) => variable(sym) + case IfLike(_, _, branches) => split(branches) + case New(_, _, refinement) => refinement.foreach: (symbol, body) => + symbol.defn.fold(visit(body.blk))(visit) + case Rcd(_, stats) => stats.foreach(visit) + case field: RcdField => field.tsym.defn.foreach(visit) + case _ => () + statement.subStatements.foreach(visit) + // Legacy expansion can insert executable terms while sharing their source children. + statement match + case term: Resolvable => visit(term.expanded) + case _ => () + + visit(root) + +/** Inference-only inputs to the representation pass. Each source slot owns a host in its + * defining compilation unit. Forwarding listeners capture only that host; imported inference + * writes to the consumer's publisher copy, never to these completed original candidates. + * No erased types or mutable bookkeeping are stored on source symbols during resolution. + */ +private[hkmc2] final class ErasureInputs: + private val values = mutable.Map.empty[Symbol, TermShapeHost] + + // Includes property declarations synthesized by named-tuple resolution, which + // have source expressions but are not separate nodes in the source syntax. + def symbols: Ls[Symbol] = values.keysIterator.toList + + private[semantics] def register(symbol: Symbol)(connect: ResolutionContext.Listener => Unit) + (using ResolutionContext): Unit = if !values.contains(symbol) then + val host = new TermShapeHost + values(symbol) = host + // Even an unused/unproductive definition has a completed, empty observation. + host.inferenceHost + connect(host.publish) + + /** Whether `symbol` is a source slot of this compilation unit that was prepared for erasure. */ + def isPrepared(symbol: Symbol): Bool = values.contains(symbol) + + def shapes(symbol: Symbol): Ls[TermShape] = + val host = values.getOrElse(symbol, lastWords("Source slot was not prepared for erasure")) + assert(host.originalData.completed, "Erasure requires completed value observations") + host.shapes.toList + diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/InterfaceExposure.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/InterfaceExposure.scala index 82a1745d4b..61f1c6d6b5 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/InterfaceExposure.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/InterfaceExposure.scala @@ -105,11 +105,25 @@ final class InterfaceExposure(resolver: NewResolver)(using ResolutionContext, TL private def members(cls: ClassLikeDef, marks: Ls[Marks], path: Path)(using ResolutionContext): Unit = cls.body.blk.stats.foreach: - case d: Definition if InterfaceExposure.isPublic(d) => member(d.bsym, marks, path.via(msg"Member '${d.bsym.nme}' is accessible here." -> d.headerLoc)) + case d: Definition if InterfaceExposure.isPublic(d) => + member(d.bsym, marks, path.via(msg"Member '${d.bsym.nme}' is accessible here." -> d.headerLoc)) + d match + // Diagnostics about the field are located at the field itself. + case td: TermDefinition if td.k is syntax.MutVal => assignable(td.tsym, td.bsym, path) + case _ => () case _ => () cls.ext.foreach: parent => term(parent, marks, path.via(msg"This parent contributes inherited members." -> parent.toLoc)) + /** Other compilation units can assign values of unknown shape to an exposed assignable field. The unknown value + * refers to the field itself, which distinguishes it from the unknown values that this compilation unit assigns + * to the field (see `Erasure.checkExposedMutableField`). Being self-contained, it reaches the field through no + * particular member lookup. + */ + private def assignable(field: TermSymbol, symbol: BlockMemberSymbol, path: Path)(using ResolutionContext): Unit = + if field.getState is rctx.owner then + resolver.writeField(field, UnknownValueShape(source(symbol), fromPublicInterface = true)(path), Nil) + private def result(body: Term, sign: Opt[Term], marks: Ls[Marks], path: Path)(using ResolutionContext): Unit = sign match case N => watch((new Identity(body), marks, "result"))(resolver.listenResult(body)): shape => @@ -170,12 +184,39 @@ final class InterfaceExposure(resolver: NewResolver)(using ResolutionContext, TL val field = segment.element watch((tuple, field, marks))(resolver.listenTupleField(field)): shape => emit(shape.exit(marks), path.via(msg"This value is stored in this tuple." -> tuple.source.toLoc)) - case record: RecordShape => record.elements.foreach: - case RecordShape.Field(field) => contextualTerm(field.rhs, marks, - path.via(msg"This value is stored in this record field." -> field.toLoc), instances.withOverrides(record.instances)) - case RecordShape.Spread(inner, context) => - emit(resolver.instantiateShape(inner, instances.withOverrides(record.instances)).exit(context).exit(marks), path) - case _ => () + case record: RecordShape => + // A spread copies fields, not its source record. Like member lookup, exposure follows the last value + // written at each key; exposing the whole source would also expose values replaced by later fields. + // Unknown keys cannot prove that an earlier field is overwritten, so retain both possible values. + // Preserve spread paths and substitutions for assigned values as well as initializers: a mutable copy + // can expose writes to an originally immutable field, and a generic source interprets them in its scope. + val mutableCopy = record.source.mut + // A record can spread the same producer repeatedly. Memoize each contextual view and overwrite set so + // nested copies walk the shared graph rather than expanding it into a tree. Empty marks add no context. + val visited = mutable.Map.empty[(RecordShape, Bool, Bool, Ls[Marks], TypeSubstitution, Set[MemberKey]), Set[MemberKey]] + def fields(record: RecordShape, copied: Bool, spread: Bool, context: Ls[Marks], substitution: TypeSubstitution, + shadowed: Set[MemberKey]): Set[MemberKey] = + val readAssignments = copied || record.source.mut + visited.getOrElseUpdate((record, readAssignments, spread, context.filter(_ != NoMarks), substitution, shadowed), + record.elements.reverse.foldLeft(shadowed): (shadowed, element) => + element match + case RecordShape.Field(field) => + if !field.key.exists(shadowed) then + val fieldPath = path.via(msg"This value is stored in this record field." -> field.toLoc) + contextualTerm(field.rhs, context, fieldPath, substitution) + if readAssignments || field.mut then + watch((field.tsym, context, substitution, "assigned values"))( + listener => resolver.listenAssignedValues(field.tsym, source(field.sym).resSym, context)(listener)( + using rctx.withInstances(substitution))): value => + emit(resolver.instantiateShape(value, substitution), fieldPath) + // A copy reads its source's assigned values, but only the exposed record's own mutability + // admits external writes. An immutable spread copy does not expose its mutable source. + if mutableCopy || (!spread && field.mut) then assignable(field.tsym, field.sym, path) + shadowed ++ field.key + case RecordShape.Spread(inner, marks) => + fields(inner, readAssignments, true, marks :: context, substitution.withOverrides(inner.instances), shadowed) + case _ => shadowed) + fields(record, false, false, marks, instances.withOverrides(record.instances), Set.empty) case base: BaseShape => members(base.defn, marks, path) case callable: CallableTypeShape => // Written structure constrains external values; missing output parts diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala index 2e36fc1fc3..8c18c06e8b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala @@ -41,6 +41,52 @@ class NewResolver: // These results carry no value flow or lexical context. Reuse one immutable // shape so repeated subscriptions do not manufacture distinct unit candidates. private lazy val unitResultShape = IntroShape(unit, N) + + /** Observe source slots while inference is still open. Interface exposure can subsequently + * add unknown inputs, and these subscriptions retain those alternatives too. Expanding an + * InstanceShape here uses the resolver's contextual type interpreter; erasure must not + * reconstruct substitutions or install new listeners after the block has been completed. + */ + def prepareErasure(root: Statement)(using ResolutionContext): Unit = + val declarations = new ErasureDeclarations(root) + def variable(symbol: VarSymbol): Unit = if !symbol.isErased then + val ref = SimpleRef(symbol)(N) + observeErasure(symbol)(listenTerm(ref)) + declarations.parameters.values.foreach: p => + variable(p.sym) + declarations.variables.foreach: + case symbol: VarSymbol => variable(symbol) + case symbol: TermSymbol if !symbol.isErased => observeErasure(symbol)(listenMemberVariable(symbol)) + // A temporary that elaboration declares (see ErasureDeclarations) is observed like a variable, + // unless its representation was fixed when it was created. + case symbol: TempSymbol if symbol.erasedType.isEmpty => + observeErasure(symbol)(listenTerm(SimpleRef(symbol)(N))) + case _ => () + declarations.statements.foreach: + case td: TermDefinition => observeErasureDefinition(td) + case _ => () + + private def observeErasure(symbol: Symbol)(connect: Listener => Unit)(using ResolutionContext): Unit = + if symbol.getState is rctx.owner then rctx.state.erasureInputs.register(symbol): publish => + connect(shape => listenInstanceViews(shape)(publish)) + + private def observeErasureDefinition(td: TermDefinition)(using ResolutionContext): Unit = + if !td.tsym.isErased then observeErasure(td.tsym): publish => + // The function body returns its payload. AsyncLowering wraps that body later; + // the callable's promise representation is assigned separately by Erasure. + td.body.foreach(body => listenResult(body)(publish)) + // A field that assignments can replace also holds the values assigned to it, including those that other + // compilation units can assign when it is exposed (see `assignField` and `InterfaceExposure`). Assignments + // in later blocks and in other compilation units reach consumer copies of the field's host instead, which + // the field's representation does not include: lowering converts their values to that representation. + if isAssignableField(td) then listenMemberVariable(td.tsym)(publish) + + /** Whether assignments can replace the value of the field that `td` defines: a mutable field (`mut val`, + * including a mutable constructor parameter) or a record field. Even the fields of an immutable record are + * assignable, through a mutable copy of the record (`mut {...r}`), which reuses its fields (see RecordMember). + */ + private def isAssignableField(td: TermDefinition): Bool = + (td.k is syntax.MutVal) || (td.k is syntax.RecordField) /** Interpret types through completed symbolic candidates, independently of term overloads. */ def typeResolution(term: Term)(using rs: ResolutionContext): TypeResolution = @@ -148,6 +194,9 @@ class NewResolver: case Spd(_, sign, _) => TypeShape.TupleField(typeResolution(sign), spread = true) case record: Rcd => val fields = record.stats.collect { case field: RcdField => field } + fields.foreach: field => + if !record.mut && !field.mut && field.rhs.withoutCaptures.isInstanceOf[WildcardTy] then + result.fail(msg"A field with input/output bounds must be declared 'mut'." -> field.toLoc :: Nil) if fields.length != record.stats.length || fields.exists(_.key.isEmpty) then result.fail(msg"A record type requires statically named fields." -> record.toLoc :: Nil) @@ -650,6 +699,19 @@ class NewResolver: (using ResolutionContext): DeclaredType = declaredType(resolution, context.bindings, positive, asArgument = true).instantiate(context.instances).withOrigin(context.origin) + /** Split a field before substitution selects either endpoint. Flipping an already substituted read type + * cannot recover its write type: {mut f: A} under A = in S out T has input S and output T. + * The enclosing polarity also applies inside explicit bounds and function types. + */ + private def declaredField(resolution: TypeResolution, context: DeclaredType, mutable: Bool) + (using ResolutionContext): TypeArgument = + val argument = declaredArgument(resolution, context, context.positive) + val output = selectArgument(argument, true) + val read = resolution.source.withoutCaptures match + case WildcardTy(_, N) => output.withOrigin(S(TypeInterfaceReason.MissingFieldOutput(resolution.source))) + case _ => output + TypeArgument(if mutable then selectArgument(argument, false) else extremeType(false), read) + private def normalizedArgument(source: TypeResolution, parts: TypeArgument)(using ResolutionContext): DeclaredType = rctx.state.argumentTypes.getOrElseUpdate((source, parts), { val normalized = new TypeResolution(source.source, source.fail) @@ -1025,7 +1087,8 @@ class NewResolver: case record: RecordTypeShape => val projected = project(instances, shapeDependencies(record), TypeSubstitution.empty) if projected.isEmpty then record else rctx.shapeView(projected): - RecordTypeShape(record.fields.map((name, tpe) => name -> instantiateType(tpe, projected)), record.aliases)(record.source, record.declarations) + RecordTypeShape(record.fields.map((name, tpe) => name -> tpe.map(instantiateType(_, projected))), + record.mutable, record.aliases)(record.source, record.declarations) case view: IntersectionView => IntersectionView(view.values.map(instantiateShape(_, instances))) case specialized: SpecializedShape => @@ -1043,7 +1106,7 @@ class NewResolver: DeclaredTypeParameter(parameter.parameter, parameter.lower.map(instantiate), parameter.upper.map(instantiate))) CallableTypeShape(callable.source, callable.paramLists.ne_map: params => params.mapTypes(instantiate) - , callable.result.map(instantiate), scheme, callable.supplied, callable.declaration, callable.asyncResult) + , callable.result.map(instantiate), scheme, callable.supplied, callable.declaration, callable.wrappedResult) case literal: IntroShape if !literal.trm.isInstanceOf[Lam] => literal case _: (UnknownValueShape | DynShape | OpaqueTypeShape | ErrShape) => source case source: ContextualSource => contextualView(source, source, TypeSubstitution.empty, instances) @@ -1190,7 +1253,7 @@ class NewResolver: val declared = nominal.bindings.values.foldLeft[Binders](lexicalBinders(nominal.defn.sym))((acc, bound) => union(acc, typeSupport(bound))) union(declared, nominal.parent.fold(S(Set.empty))(shapeSupport)) case record: RecordTypeShape => - record.fields.values.foldLeft[Binders](S(Set.empty))((acc, field) => union(acc, typeSupport(field))) + record.fields.values.foldLeft[Binders](S(Set.empty))((acc, field) => union(acc, union(typeSupport(field.input), typeSupport(field.output)))) case view: IntersectionView => view.values.foldLeft[Binders](S(Set.empty))((acc, value) => union(acc, shapeSupport(value))) case callable: CallableTypeShape => @@ -1316,15 +1379,19 @@ class NewResolver: case value => value :: Nil val records = flattened.collect: case Marked(record: RecordTypeShape, marks) => - RecordTypeShape(record.fields.map((name, tpe) => name -> transportType(tpe, marks :: Nil)), record.aliases)(record.source, record.declarations) + RecordTypeShape(record.fields.map((name, tpe) => name -> tpe.map(transportType(_, marks :: Nil))), + record.mutable, record.aliases)(record.source, record.declarations) val merged = records.headOption.map: first => val merged = records.tail.foldLeft(first): (left, right) => val fields = right.fields.foldLeft(left.fields): case (fields, (name, tpe)) => fields.updated(name, fields.get(name).fold(tpe)(previous => - combinedType(previous.resolution, previous, tpe, false))) + // Both interfaces can write the field, while a read must satisfy both output types. + TypeArgument(combinedType(previous.input.resolution, previous.input, tpe.input, true), + combinedType(previous.output.resolution, previous.output, tpe.output, false)))) val aliases = right.aliases.foldLeft(left.aliases): case (aliases, (name, keys)) => aliases.updated(name, aliases.getOrElse(name, Set.empty) ++ keys) - RecordTypeShape(fields, aliases)(left.source, right.declarations ++ left.declarations) + // A field is mutable in an intersection if any of its components declares it so. + RecordTypeShape(fields, left.mutable ++ right.mutable, aliases)(left.source, right.declarations ++ left.declarations) merged val components = merged.toList ::: flattened.filter: case Marked(_: RecordTypeShape, _) => false @@ -1509,7 +1576,9 @@ class NewResolver: case (field @ RcdField(Lit(Tree.StrLit(name)), _, _), _) => field.key.collect: case key: SymbolKey => name -> Set(key) case _ => N - publish(RecordTypeShape(fields.map((field, sign) => field.key.get -> declaredType(sign, current)).toMap, aliases.toMap)( + publish(RecordTypeShape(fields.map((field, sign) => + field.key.get -> declaredField(sign, current, field.mut || source.mut)).toMap, + fields.collect { case (field, _) if field.mut || source.mut => field.key.get }.toSet, aliases.toMap)( source, fields.map((field, _) => field.key.get -> field).toMap)) case TypeShape.Union(left, right) => next(left); next(right) case TypeShape.Intersection(left, right) if !mergeIntersections => next(left); next(right) @@ -1635,6 +1704,24 @@ class NewResolver: msg"This async annotation wraps the body's result in a promise." -> N :: Nil).withTypeOrigin(annotation) NominalInstanceView(cls, Map(cls.tparams.head.sym -> awaitedType(payload)), implicitParent(cls))(N, provenance)(this) + /** A generator call returns an iterator over its yielded values. Neither the yielded + * values nor the body's final payload are tracked, so the iterator yields `Any`. + */ + private def iteratorShape(annotation: Annot.Generator)(using ResolutionContext): NominalInstanceView = + val cls = prelude.builtins.Iterator.defn.get + softAssert(cls.tparams.length == 1 && cls.ext.isEmpty, + "The builtin Iterator must have one element parameter and the implicit Object parent") + val provenance = ShapeProvenance( + msg"This generator annotation makes calls return an iterator instead of the body's result." -> N :: Nil) + .withTypeOrigin(annotation) + NominalInstanceView(cls, Map(cls.tparams.head.sym -> extremeType(true)), implicitParent(cls))(N, provenance)(this) + + /** The object that replaces a function's result once all its written parameter lists are applied. */ + private def wrapperShape(annotation: Annot.Async | Annot.Generator, payload: DeclaredType) + (using ResolutionContext): NominalInstanceView = annotation match + case annotation: Annot.Async => promiseShape(annotation, payload) + case annotation: Annot.Generator => iteratorShape(annotation) + /** Reify the projection before observing its operand. Expanding an inferred * body's type parameter here would freeze the declaration's abstract view * before a caller can substitute its own type argument. @@ -1678,6 +1765,7 @@ class NewResolver: /** Result annotations constrain the body before async lowering. Consumers of * the function observe Promise[Awaited[result]], including explicit returns. + * Consumers of a generator observe an iterator, whatever its body returns. * Inferred values retain their lexical endpoint until callers leave the scope. */ private def listenDefinitionResult(td: TermDefinition, instances: TypeSubstitution)(listener: Listener)(using ResolutionContext): Unit = @@ -1685,31 +1773,32 @@ class NewResolver: case S(sign) => listenSignatureResult(declaredType(typeResolution(sign), Map.empty).instantiate(instances), if td.flags.hasResultAnnotation then 0 else td.params.length)(listener) case N => td.body.foreach(body => listenResult(body)(listener)) - td.asyncAnnotation match - case S(annotation) => + td.resultWrapperAnnotation match + case S(annotation: Annot.Async) => val source = td.sign.orElse(td.body).get // The body and signature are written inside the definition, where its own binders are in scope. val scope = lexicalBinders(td.tsym).map(_ ++ definitionBinders(td)) listener(promiseShape(annotation, resultType(source, scope, instances)(bodyResult))) + case S(annotation: Annot.Generator) => listener(iteratorShape(annotation)) case N => bodyResult(listener) /** Observe one application of a declared callable. Partial application keeps - * the async wrapper pending, even across the nested arrows of a full signature. + * the async or generator wrapper pending, even across the nested arrows of a full signature. */ private[semantics] def listenCallableResult(callable: CallableTypeShape)(listener: Listener)(using ResolutionContext): Unit = - callable.asyncResult match - case S(AsyncResult(annotation, 1)) => + callable.wrappedResult match + case S(WrappedResult(annotation, 1)) => softAssert(callable.paramLists.tail.isEmpty) - // A nominal member without a result annotation still returns Promise, + // A nominal member without a result annotation still returns its wrapper, // but its interface must not recover a payload type from the hidden body. - listener(promiseShape(annotation, callable.result.getOrElse(extremeType(true)))) + listener(wrapperShape(annotation, callable.result.getOrElse(extremeType(true)))) case pending => - val remaining = pending.map(info => AsyncResult(info.annotation, info.remainingLists - 1)) + val remaining = pending.map(info => WrappedResult(info.annotation, info.remainingLists - 1)) callable.paramLists.tail match - case tail: NELs[DeclaredParams] => listener(callable.copy(paramLists = tail, asyncResult = remaining)) + case tail: NELs[DeclaredParams] => listener(callable.copy(paramLists = tail, wrappedResult = remaining)) case Nil => callable.result match case S(tpe) if remaining.nonEmpty => listenTypeViews(tpe): - case Marked(next: CallableTypeShape, marks) => listener(Marked(next.copy(asyncResult = remaining), marks)) + case Marked(next: CallableTypeShape, marks) => listener(Marked(next.copy(wrappedResult = remaining), marks)) case other => listener(other) case S(tpe) => listenTypeInstances(tpe)(listener) case N => listener(UnknownValueShape.at(callable.source)) @@ -1790,7 +1879,7 @@ class NewResolver: val parameters = td.tparams.toList.flatten.map(p => declaredParameter(p.sym)) ::: callable.tparams CallableTypeShape(callable.source, callable.paramLists, callable.result, if parameters.isEmpty then N else S(TypeScheme(td.tsym, parameters)), callable.supplied, S(td), - td.asyncAnnotation.map(AsyncResult(_, td.params.length))) + td.resultWrapperAnnotation.map(WrappedResult(_, td.params.length))) case instance: InstanceShape if !isByName(td) => instanceShape(quantifiedType(instance.tpe, td.tparams.toList.flatten.map(_.sym))) case _ => interface @@ -1910,7 +1999,7 @@ class NewResolver: val instantiated = CallableTypeShape(callable.source, callable.paramLists.ne_map: params => params.mapTypes(instantiate) - , callable.result.map(instantiate), N, N, callable.declaration, callable.asyncResult) + , callable.result.map(instantiate), N, N, callable.declaration, callable.wrappedResult) // Reentrant observations must find the view before supplied arguments // can publish candidates or trigger another observation of this call. rctx.state.instantiatedCallables(key) = instantiated @@ -2005,7 +2094,8 @@ class NewResolver: // candidates through listenConstraintViews, avoiding candidate products. case TypeShape.Combined(formula) => formula.orderedAtoms.foreach(atom => follow(atom.instantiate(tpe.instances), noTypeArguments, captures, next)) - case TypeShape.Record(_, fields) => observe(constrainRecord(fields.map((field, sign) => field -> declaredType(sign, tpe)), _, marks, site)) + case TypeShape.Record(source, fields) => observe(constrainRecord(fields.map((field, sign) => + (field, declaredField(sign, tpe, field.mut || source.mut), field.mut || source.mut)), _, marks, site)) case TypeShape.Function(_, _) => observe(constrainFunction(tpe, _, marks, site)) case TypeShape.Polymorphic(_, _, body) => follow(declaredType(body, tpe), args, captures, next) @@ -2111,18 +2201,30 @@ class NewResolver: /** Structural constraints follow declared fields only. This supplies generic * argument inference and callback checking without refining the annotation * with additional properties from an actual record. + * The values that a record type describes can be assigned values of the input types of its `mutable` fields + * (see RcdField) through it, so the actual value's assignable field receives them, just like the + * parameters of an actual callback receive values of their declared types (see `constrainFunction`). + * Assignments through the immutable fields of record types are assumed not to happen: type checking will + * reject them. Writes through structural interfaces need not contribute their individual values to + * concrete fields, but still constrain assigned callbacks against the input type (see `assignField`). */ - private def constrainRecord(fields: Ls[(RcdField, DeclaredType)], + private def constrainRecord(fields: Ls[(RcdField, TypeArgument, Bool)], actual: TermShape, marks: Ls[Marks], site: Opt[Term])(using ResolutionContext): Unit = - fields.foreach: (field, expected) => + fields.foreach: (field, expected, mutable) => val flow = rctx.fieldProjectionSite(field.sym) - def receive(value: TermShape)(using ResolutionContext): Unit = inferTypeArguments(expected, value, marks, site) + def receive(value: TermShape)(using ResolutionContext): Unit = inferTypeArguments(expected.output, value, marks, site) actual match case Marked(_: UnknownValueShape, _) => receive(actual) - // A structural interface only describes public members. - case _ => listenMemberValue(actual.getMember(field.key.get).visibleFrom(_ => false, reportInaccessible = false), - flow, field.rhs, TypeSubstitution.empty, () => ())(receive) + case _ => + // A structural interface only describes public members. + val lookup = actual.getMember(field.key.get).visibleFrom(_ => false, reportInaccessible = false) + listenMemberValue(lookup, flow, field.rhs, TypeSubstitution.empty, () => ())(receive) + if mutable then + assignableTargets(lookup): (target, inner) => + listenTypeInstances(expected.input)(writeField(target, _, inner)) + listenFieldInput(lookup, field.rhs): input => + listenTypeInstances(expected.input)(inferTypeArguments(input, _, marks, site)) /** Read a member using the same declaration and scope rules for ordinary * constraints and implicit protocol observations. The caller decides whether @@ -2133,16 +2235,21 @@ class NewResolver: (using ResolutionContext): Unit = info match case MemberLookup.All(members) => members.foreach(listenMemberValue(_, flow, source, instances, unresolved)(receive)) case MemberLookup.Contextual(nested, substitution) => listenMemberValue(nested, flow, source, instances.withOverrides(substitution), unresolved)(receive) - case MemberLookup.Found(RecordMember(member, true), _) => receive(UnknownValueShape.at(member.rhs)) case MemberLookup.Found(member, inner) => - fromBMS(member.memberSymbol, flow, inner, value => receive(instantiateShape(value, instances)), source, _ => (), false)(using rctx.withInstances(instances)) + def read(value: TermShape)(using ResolutionContext): Unit = receive(instantiateShape(value, instances)) + fromBMS(member.memberSymbol, flow, inner, read, source, _ => (), false)(using rctx.withInstances(instances)) + // Like other reads through mutable lookups (see `publishMember`). + member match + case RecordMember(member, true) => + listenAssignedValues(member.tsym, flow, inner)(read)(using rctx.withInstances(instances)) + case _ => () case MemberLookup.Declared(member, bound, inner, annotation, positive) => listenDeclaredMember(member, bound, flow, source, annotation, _ => (), false, positive, inner, N): value => transportShape(instantiateShape(value, instances), inner) match case value: TermShape => receive(value) case NoShape => () case MemberLookup.Typed(_, tpe, inner) => - receive(instanceShape(transportType(instantiateType(tpe, instances), inner))) + receive(instanceShape(transportType(instantiateType(tpe.output, instances), inner))) case MemberLookup.Dynamic(inner) => DynShape().exit(inner) match case value: TermShape => receive(value) case NoShape => () @@ -2202,7 +2309,7 @@ class NewResolver: case _ => matchLists(ret, tail) case Nil => () case Marked(record: RecordTypeShape, _) => - constrainRecord(record.fields.iterator.map((name, tpe) => record.declarations(name) -> tpe).toList, + constrainRecord(record.fields.iterator.map((name, tpe) => (record.declarations(name), tpe, record.mutable(name))).toList, actual, marks, site) case _ => () actual match @@ -3200,6 +3307,85 @@ class NewResolver: case _ => inferTypeArguments(instanceType(instance), instantiateShape(bound, substitution), marks, N) substitution + /** The type of the instances of a primitive class, whose views locate their provenance at `source`. */ + private def primitiveType(cls: ClassSymbol, source: Term)(using ResolutionContext): DeclaredType = + val resolution = new TypeResolution(source, messages => resolError(source, messages)) + resolution.publish(TypeShape.Nominal(cls.defn.get)) + declaredType(resolution, Map.empty) + + /** The declared interface of a primitive class. */ + private def listenPrimitiveInterface(tpe: DeclaredType)(listener: NominalInstanceView => Unit) + (using ResolutionContext): Unit = + listenTypeViews(tpe): + case nominal: NominalInstanceView => listener(nominal) + case _ => softAssert(false, "Primitive class must expose a nominal interface") + + /** WORKAROUND (see BuiltinOperatorResults): builtin operators have no declared interface, so without this, + * their applications would have no shapes at all, and a slot they flow into would only be inferred from its + * other inputs. When targeting JavaScript, approximate the results from the primitive classes of the operands. + * Applications the rules cannot approximate produce unknown values, as do all applications when targeting + * Wasm, whose backend implements the operators with dynamically typed runtime intrinsics. + */ + def builtinApplication(op: BuiltinSymbol, args: Term, res: App)(using ResolutionContext): Unit = + import BuiltinOperatorResults.{PrimitiveClass, Alternative} + def publish(shape: TermShape)(using ResolutionContext): Unit = publishViewed(res, shape, rctx.instances) + var publishedUnknown = false + def unknown()(using ResolutionContext): Unit = if !publishedUnknown then + publishedUnknown = true + publish(UnknownValueShape.at(res)) + val forwarded = mutable.Set.empty[Int] + // Unlike literals, which share their class's cached interface, results locate their provenance at the application. + val resultTypes = mutable.Map.empty[PrimitiveClass, DeclaredType] + val operands: Opt[Ls[Term]] = args match + case Tup(fields) => fields.foldRight(S(Nil): Opt[Ls[Term]]): + case (Fld(_, value, _), S(rest)) => S(value :: rest) + case _ => N + case _ => N + def produce(terms: Ls[Term], approximation: Opt[Ls[Alternative]])(using ResolutionContext): Unit = + approximation match + case S(alternatives) => alternatives.foreach: + case Alternative.Of(primitive) => + val builtins = prelude.builtins + val cls = primitive match + case PrimitiveClass.Int => builtins.Int + case PrimitiveClass.Num => builtins.Num + case PrimitiveClass.Str => builtins.Str + case PrimitiveClass.Bool => builtins.Bool + case PrimitiveClass.BigInt => builtins.BigInt + listenPrimitiveInterface(resultTypes.getOrElseUpdate(primitive, primitiveType(cls, res)))(publish) + case Alternative.Operand(index) => if forwarded.add(index) then listenTerm(terms(index))(publish) + case N => unknown() + def primitiveOf(view: TermShape)(using ResolutionContext): Opt[PrimitiveClass] = + val builtins = prelude.builtins + def is(cls: ClassSymbol) = view.isInstanceOfClass(cls.defn.get) + if is(builtins.Int) then S(PrimitiveClass.Int) + else if is(builtins.Num) then S(PrimitiveClass.Num) + else if is(builtins.Str) then S(PrimitiveClass.Str) + else if is(builtins.Bool) then S(PrimitiveClass.Bool) + else if is(builtins.BigInt) then S(PrimitiveClass.BigInt) + else N + operands match + case S(terms) if (config.target is CompilationTarget.JS) && BuiltinOperatorResults.approximates(op.nme, terms.length) => + BuiltinOperatorResults(op.nme, terms.map(_ => N)) match + // Approximations that hold for operands of any class do not wait for the operands' shapes. + case S(alternatives) => produce(terms, S(alternatives)) + case N => + // Combine each class newly seen for one operand with the classes already seen for the others. Classes + // seen in different activations are combined as well, which over-approximates the results. An operand + // without any shapes yields no results, like any other use of a value without shapes. + val seen = terms.map(_ => mutable.LinkedHashSet.empty[Opt[PrimitiveClass]]) + terms.zipWithIndex.foreach: (term, index) => + listenTerm(term): shape => + listenInstanceViews(shape): view => + val primitive = primitiveOf(view) + if seen(index).add(primitive) then + val choices = seen.zipWithIndex.map: (classes, other) => + if other == index then primitive :: Nil else classes.toList + choices.foldRight(Nil :: Nil: Ls[Ls[Opt[PrimitiveClass]]]): (options, suffixes) => + options.flatMap(option => suffixes.map(option :: _)) + .foreach(classes => produce(terms, BuiltinOperatorResults(op.nme, classes))) + case _ => unknown() + def appShape(source: TermShape, args: Term, res: App)(using ResolutionContext): Unit = applyShape(source, args, res, checked = true) @@ -3371,13 +3557,12 @@ class NewResolver: publish(symShapes.getOrElseUpdate((sym, flow, marks), CoreSymShape(sym, flow, marks))) member match case member: BlockMemberSymbol => definition(member) - case RecordMember(field, false) => definition(field.sym) - case RecordMember(field, true) => - // Assignments can replace the stored value, so its initializer no longer - // determines what a read returns. Publish the known property symbol for - // selection/assignment, but an unknown shape for the value being read. - rctx.recordResolution(host, host.resolvedTargets.contains(field.tsym))(host.resolvedTargets ::= field.tsym) - publish(UnknownValueShape.at(field.rhs)) + case RecordMember(field, mutable) => + definition(field.sym) + // Through a mutable lookup, a read also returns the values assigned to the field (see `writeField`), + // like a read of a mutable class field (see `fromBMSAt`). The field's own definition cannot know whether + // a read is mutable, as mutable copies of a record reuse its fields (see RecordMember). + if mutable then listenAssignedValues(field.tsym, flow, marks)(publish) private def publishDeclared(host: NewResolvable & ShapeHost, member: BlockMemberSymbol, flow: FlowSymbol, bindings: Map[VarSymbol, DeclaredType], marks: Ls[Marks], annotation: Opt[Term], positive: Bool, @@ -3437,7 +3622,7 @@ class NewResolver: case MemberLookup.Typed(field, tpe, marks) if rctx.canResolve(ref) || ref.resolvedMembers.contains(prefix -> field.sym) => val candidate = prefix -> field.sym rctx.recordResolution(ref, ref.resolvedMembers.contains(candidate))(ref.resolvedMembers ::= candidate) - publishTyped(ref, field, tpe, marks, instances) + publishTyped(ref, field, tpe.output, marks, instances) case MemberLookup.Indexed(_, _) if rctx.canResolve(ref) => resolError(ref, msg"Tuple elements must be selected by index." -> ref.toLoc :: Nil) case MemberLookup.Dynamic(marks) if rctx.canResolve(ref) || ref.dynamicPrefixes.contains(prefix) => @@ -3591,7 +3776,7 @@ class NewResolver: publishDeclared(sel, member, sel.resSym, bindings, marks, annotation, positive, instances) case MemberLookup.Typed(field, tpe, marks) if rctx.canResolve(sel) || sel.resolvedMembers.contains(field.sym) => rctx.recordResolution(sel, sel.resolvedMembers.contains(field.sym))(sel.resolvedMembers ::= field.sym) - publishTyped(sel, field, tpe, marks, instances) + publishTyped(sel, field, tpe.output, marks, instances) case MemberLookup.Indexed(fields, marks) if rctx.canResolve(sel) || sel.tupleIndex == sel.id.name.toIntOption => val index = sel.id.name.toIntOption softAssert(index.exists(_ >= 0), "Tuple lookup must identify a nonnegative index") @@ -3644,31 +3829,38 @@ class NewResolver: bms.asModOrObj.orElse(bms.asTrm).orElse(bms.asCls).foreach: sym => rctx.recordResolution(sel, sel.resolvedTargets.contains(sym))(sel.resolvedTargets ::= sym) listenTermViews(sel.prefix): receiver => - val selected = receiver match - case Marked(nominal: NominalInstanceView, context) => - nominal.ancestor(cd) match - case S(view) => visible(view.getMember(sel.id.name)).withAnnotation(nominal.annotation).withMarks(context :: Nil) - case N => - // An explicit projection can name a narrower class, but - // it cannot recover that class's implementation arguments. - val opaque = abstractType(new TypeResolution(sel, msgs => resolError(sel, msgs))) - MemberLookup.Declared(bms, cd.tparams.map(_.sym -> opaque).toMap, context :: Nil, nominal.annotation, true) - case _ => - // C# selects a declaration, not an instance of C. Transport - // the member through the actual receiver, never the qualifier. - projectedMember(receiver, cd, sel.id.name).map(visible).getOrElse: - // Without a path into this class, only its declared - // interface is available; qualifier values add no evidence. - val opaque = abstractType(new TypeResolution(sel, msgs => resolError(sel, msgs))) - MemberLookup.Declared(bms, cd.tparams.map(_.sym -> opaque).toMap, - declarationMarks ::: ExitMark(ResolutionBoundary(cd.sym), N, NoMarks) :: Nil, N, true) - member(selected, msg"Class '${cd.sym.nme}'", message => (message -> cd.headerLoc) :: Nil, TypeSubstitution.empty) + member(projectedLookup(sel, cd, bms, declarationMarks, receiver, visible), + msg"Class '${cd.sym.nme}'", message => (message -> cd.headerLoc) :: Nil, TypeSubstitution.empty) case _ => member(info, msg"Class '${cd.sym.nme}'", message => (message -> cd.headerLoc) :: Nil, TypeSubstitution.empty)) , sh => rctx.markError(sel) resolError(sel, sh.diagnostic(msg"${sh.describe.capitalize} cannot be used as a projection class.")) ) + /** The member that a projection `sel` (`o.C#m`) selects in `receiver`, one of the receivers of `o`, where `C` + * resolves to `cd` and `m` to the member `bms` of `cd`, with the marks `declarationMarks`. `visible` applies the + * private-member visibility of the scope where `sel` is written. + */ + private def projectedLookup(sel: NewSel, cd: ClassDef, bms: BlockMemberSymbol, declarationMarks: Ls[Marks], + receiver: TermShape, visible: MemberLookup => MemberLookup)(using ResolutionContext): MemberLookup = receiver match + case Marked(nominal: NominalInstanceView, context) => + nominal.ancestor(cd) match + case S(view) => visible(view.getMember(sel.id.name)).withAnnotation(nominal.annotation).withMarks(context :: Nil) + case N => + // An explicit projection can name a narrower class, but + // it cannot recover that class's implementation arguments. + val opaque = abstractType(new TypeResolution(sel, msgs => resolError(sel, msgs))) + MemberLookup.Declared(bms, cd.tparams.map(_.sym -> opaque).toMap, context :: Nil, nominal.annotation, true) + case _ => + // C# selects a declaration, not an instance of C. Transport + // the member through the actual receiver, never the qualifier. + projectedMember(receiver, cd, sel.id.name).map(visible).getOrElse: + // Without a path into this class, only its declared + // interface is available; qualifier values add no evidence. + val opaque = abstractType(new TypeResolution(sel, msgs => resolError(sel, msgs))) + MemberLookup.Declared(bms, cd.tparams.map(_.sym -> opaque).toMap, + declarationMarks ::: ExitMark(ResolutionBoundary(cd.sym), N, NoMarks) :: Nil, N, true) + /** Both constructor references and explicit `new` use the same definition and * parameter lists, including auxiliary constructor(...) lists. A partial `new` * therefore retains the same callable head and contexts as a partial C(...). @@ -3762,6 +3954,145 @@ class NewResolver: case _ => () listenTerm(rhs)(assign(lhs, _)) + /** Assignments to a field are inputs of the field, like its initializer: erasure infers the representation of a + * field without a declared type from both (see `observeErasureDefinition`). Resolve the assigned member the way + * `newSel` and `unresolvedRef` resolve selections, for every class that the receiver can be an instance of, and + * write the assigned values to each assignable field that the lookups find (see `writeField`). `scope` is where + * `lhs` is written, which determines the private members it can select. + * Dynamic receivers and computed keys have no static target field: lowering compiles such assignments without + * converting their values to the representation of a field, which only the JavaScript backend supports, as all + * its values share one representation. A record type's field belongs to the type rather than to the values it + * describes, whose fields receive any value of the field's input type if it is mutable (see `constrainRecord`). + */ + private[semantics] def assignField(lhs: Term, rhs: Term, scope: SrcScope)(using ResolutionContext): Unit = + // `transport` takes an assigned value from the assignment into the scope where the selection is evaluated: + // the selection can be captured by a nested definition, from which the assigned value exits, as for variables. + def target(lhs: Term, transport: TermShape => TermShape | NoShape)(using ResolutionContext): Unit = + def assign(field: TermSymbol, marks: Ls[Marks])(using ResolutionContext): Unit = + // Each receiver path to a field follows the assigned values once per activation of the assignment. + if rctx.state.fieldAssignments.add((new Identity(lhs), new Identity(rhs), field, marks)) then + listenTerm(rhs): value => + transport(value) match + case value: TermShape => writeField(field, value, marks) + case NoShape => () + def lookup(info: MemberLookup)(using ResolutionContext): Unit = + assignableTargets(info)(assign) + // The interface's input constrains assigned callbacks and generic arguments even when its + // implementation is hidden. Checking only the field's reads would select the opposite bound. + listenFieldInput(info, lhs): input => + if rctx.state.fieldAssignmentTypes.add((new Identity(lhs), new Identity(rhs), input)) then + listenTerm(rhs): value => + transport(value) match + case value: TermShape => inferTypeArguments(input, value, Nil, S(rhs)) + case NoShape => () + def visible(info: MemberLookup, reportInaccessible: Bool): MemberLookup = + info.visibleFrom(scope.canAccessPrivatesOf, reportInaccessible) + lhs match + case Capture(base, thru) => target(base, value => transport(value) match + case value: TermShape => MarkedShape.exit(value, ResolutionBoundary(thru), N) + case NoShape => NoShape) + // Mirror the lookups of `newSel`, which select the same members. + case sel: NewSel => + def selected(info: MemberLookup)(using ResolutionContext): Unit = lookup(visible(info, reportInaccessible = true)) + sel.cls match + case N if sel.id.name == "class" => + listenClass(sel.prefix, recordTargets = false)(_ => (), shape => selected(shape.getMember(sel.id.name))) + case N => listenReceiver(sel.prefix)(shape => selected(shape.getMember(sel.id.name))) + case S(cls) => + classSelection(cls)(ref => + val cd = ref.definition + listenExt(cd, ext => visible(DefnShape(cd, ext).getInstanceMember(sel.id.name), reportInaccessible = true) match + case MemberLookup.Found(bms: BlockMemberSymbol, declarationMarks) => + listenTermViews(sel.prefix): receiver => + lookup(projectedLookup(sel, cd, bms, declarationMarks, receiver, visible(_, reportInaccessible = true))) + case info => selected(info)) + , _ => ()) + // Mirror the lookups of `unresolvedRef`. + case ref: UnresolvedRef => ref.prefixes.foreach: prefix => + listenReceiver(prefix)(shape => lookup(visible(shape.getMember(ref.id.name), reportInaccessible = false))) + // Definitions outside class-like bodies are referenced directly (see `Ctx.withMembers`). + case MemberRef(member: BlockMemberSymbol) => lookup(MemberLookup.Found(member, Nil)) + case _ => () + target(lhs, value => value) + + /** Write `value` to the host of the assignable `field` (`memberVariable`), which holds the values assigned to the + * field as seen from inside its definition, like its initializer. Reading the field leaves its definition and + * then follows the path of the member lookup to the reader (see `fromBMSAt`), so a value assigned through a lookup + * with the path `marks` enters the field along the reverse path. A value enters the field's definition at no + * particular site: the field is not evaluated anew by each read, as a by-name definition would be. + */ + private[semantics] def writeField(field: TermSymbol, value: TermShape, marks: Ls[Marks])(using ResolutionContext): Unit = + softAssert(field.defn.exists(isAssignableField), "Only assignable fields hold assigned values") + value.enter(marks) match + case value: TermShape => + publishActivated(rctx.memberVariable(field), MarkedShape.enter(value, ResolutionBoundary(field), N)) + case NoShape => () + + /** Read the values assigned to `field` (see `writeField`) through a member lookup with the path `marks` at `site`: + * like the field's initializer (see `fromBMSAt`), they leave the field's definition and then follow that path. + */ + private[semantics] def listenAssignedValues(field: TermSymbol, site: FlowSymbol, marks: Ls[Marks])(listener: Listener) + (using ResolutionContext): Unit = + listenMemberVariable(field): value => + MarkedShape.exit(value, ResolutionBoundary(field), S(site)) match + case value: TermShape => transportShape(value, marks) match + case value: TermShape => listener(value) + case NoShape => () + case NoShape => () + + /** A mutable member's write interface, interpreted at the lookup site. A nominal signature must be + * interpreted negatively before its receiver bindings select an endpoint; a structural field already + * retains both endpoints. Use the ordinary declared-member transport so captures and constructor fields + * follow exactly the same scope path as a read. Unannotated concrete fields impose no declared input. + */ + private def listenFieldInput(info: MemberLookup, source: Term)(listener: ShapeListener[DeclaredType]) + (using ResolutionContext): Unit = + val caller = rctx + def declared(member: BlockMemberSymbol, bindings: Map[VarSymbol, DeclaredType], marks: Ls[Marks], + annotation: Opt[Term], positive: Bool, instances: TypeSubstitution)(using ResolutionContext): Unit = + member.onComplete: () => + if valueTarget(member, false).flatMap(_.defn).exists: + case td: TermDefinition => td.k is syntax.MutVal + case _ => false + then listenDeclaredMember(member, bindings, rctx.fieldProjectionSite(member), source, annotation, _ => (), + false, !positive, marks, N)( + value => value match + case Marked(instance: InstanceShape, inner) => + listener(transportType(instance.tpe, inner :: marks))(using caller) + case _ => () + )(using rctx.withInstances(instances)) + def follow(info: MemberLookup, instances: TypeSubstitution)(using ResolutionContext): Unit = info match + case MemberLookup.All(members) => members.foreach(follow(_, instances)) + case MemberLookup.Contextual(source, substitution) => follow(source, instances.withOverrides(substitution)) + case MemberLookup.Typed(_, tpe, marks) => + listener(transportType(instantiateType(tpe.input, instances), marks)) + case MemberLookup.Declared(member, bindings, marks, annotation, positive) => + declared(member, bindings, marks, annotation, positive, instances) + case MemberLookup.Found(member: BlockMemberSymbol, marks) => + declared(member, Map.empty, marks, N, true, instances) + case _ => () + follow(info, rctx.instances) + + /** The assignable fields (see `isAssignableField`) that a member lookup finds, with the paths of their lookups. + * The fields of an immutable record are only assignable through mutable lookups: assigning them through the + * immutable record has no effect. Lookups through structural types, dynamic receivers, and computed keys find + * no assignable field (see `assignField`). + */ + private def assignableTargets(info: MemberLookup)(target: (TermSymbol, Ls[Marks]) => ResolutionContext ?=> Unit) + (using ResolutionContext): Unit = + def member(member: BlockMemberSymbol, marks: Ls[Marks])(using ResolutionContext): Unit = member.onComplete: () => + valueTarget(member, false) match + case S(field: TermSymbol) if field.defn.exists(isAssignableField) => target(field, marks) + case _ => () + info match + case MemberLookup.All(members) => members.foreach(assignableTargets(_)(target)) + // The receiver's substitution interprets the field's value; assigned values keep their own. + case MemberLookup.Contextual(source, _) => assignableTargets(source)(target) + case MemberLookup.Found(RecordMember(field, mutable), marks) => if mutable then target(field.tsym, marks) + case MemberLookup.Found(found: BlockMemberSymbol, marks) => member(found, marks) + case MemberLookup.Declared(declared, _, marks, _, _) => member(declared, marks) + case _ => () + def defineVar(sym: LocalSymbol | TermSymbol, rhs: Term)(loc: Opt[Loc])(using ResolutionContext): DefineVar = if newResolution then sym match case sym: TermSymbol => @@ -4026,6 +4357,11 @@ class NewResolver: case _ => shape receive(captured) )(using rctx.withInstances(instances)) + // A mutable field holds its initializer and then the values assigned to it (see `writeField`), which + // keep the activations of their assignments, like the values assigned to a variable. Without them, + // a read could resolve a member statically in the initializer's class only, while another class's + // instance was assigned. The fields of external declarations, which have no body, are left unobserved. + if td.k is syntax.MutVal then listenAssignedValues(td.tsym, resSym, markss)(listener) case S(d: TermDefinition) if !d.tsym.isInstanceOf[ClassCtorSymbol] && ((d.sign.nonEmpty && (!d.flags.hasResultAnnotation || d.params.forall(ps => ps.allParams.forall(_.sign.nonEmpty)))) || @@ -4221,15 +4557,27 @@ class NewResolver: case _: Definition => normal case term: Term => resultFlow(term) case _ => sequence(stat.subTerms) - def split(branches: SimpleSplit, onEnd: ResultFlow): ResultFlow = branches match - case SimpleSplit.Else(value) => resultFlow(value) + def irrefutable(pattern: Pattern): Bool = pattern match + case Pattern.Wildcard() => true + case Pattern.Alias(inner, _) => irrefutable(inner) + case Pattern.Annotated(inner, _) => irrefutable(inner) + case Pattern.Composition(true, left, right) => irrefutable(left) || irrefutable(right) + case Pattern.Composition(false, left, right) => irrefutable(left) && irrefutable(right) + case _ => false + def split(branches: SimpleSplit, onEnd: ResultFlow, repeats: Bool): ResultFlow = branches match + case SimpleSplit.Else(value) => + val flow = resultFlow(value) + if repeats then flow.copy(normal = false) else flow case SimpleSplit.End => onEnd - case SimpleSplit.Cons(SimpleSplit.Head.Let(_, value), tail) => resultFlow(value).andThen(split(tail, onEnd)) + case SimpleSplit.Cons(SimpleSplit.Head.Let(_, value), tail) => resultFlow(value).andThen(split(tail, onEnd, repeats)) case SimpleSplit.Cons(SimpleSplit.Head.Match(scrutinee, pattern, consequent), tail) => // A failed pattern can skip its guards. Retain that path as well as // any control transfer from a guard which is evaluated. val guards = pattern.subTerms.foldLeft(normal)((flow, term) => flow.or(resultFlow(term))) - resultFlow(scrutinee).andThen(guards.andThen(split(consequent, onEnd).or(split(tail, onEnd)))) + val alternative = split(tail, onEnd, repeats) + val matched = split(consequent, alternative, repeats) + resultFlow(scrutinee).andThen(guards.andThen( + if irrefutable(pattern) then matched else matched.or(alternative))) def quoted(term: Term, depth: Int): ResultFlow = term match // Only splices crossing back to the current stage execute now. Nested // quotations delay their own splices, including any returns they contain. @@ -4254,9 +4602,9 @@ class NewResolver: if last.normal then first.or(last.copy(normal = false)) else last case IfLike(_, form, branches) => // Falling off an imperative split returns unit; falling off a returning - // split throws a match error. A loop may finish without running its body. - val flow = split(branches, if form.isImperative then normal else ResultFlow(false, Map.empty)) - if form == IfLikeForm.While then flow.or(normal) else flow + // split throws a match error. A loop's completed branch starts another + // iteration; only falling off the split can produce a normal unit result. + split(branches, if form.isImperative then normal else ResultFlow(false, Map.empty), form == IfLikeForm.While) case Quoted(body) => quoted(body, 1) case _: Lam | _: Unquoted | _: FunTy | _: Forall | _: Constrained | _: WildcardTy | _: CompType | _: Neg | _: DynTy => normal @@ -4269,6 +4617,8 @@ class NewResolver: case TyApp(value, _) => resultFlow(value) case _ => sequence(term.subTerms) + protected def completesNormally(term: Term): Bool = resultFlow(term).normal + /** Function results include explicit returns, whereas listenTerm observes only * normal completion. Share this rule between calls, result checking, getters, * and interface exposure so a returned closure is checked even when no caller @@ -4429,7 +4779,12 @@ class NewResolver: assert(graph != null, "A tuple producer must have an inference owner") val defining = rctx.inGraph(graph.nn.context) val record = defining.state.namedTupleRecords.getOrElseUpdate(new Identity(tuple), { - val record: Rcd = Rcd(false, fields.map((key, value) => RcdField(key, value)(using rctx.owner))) + val record: Rcd = Rcd(false, fields.map((key, value) => + // Like the record they are packed into at run time, named tuple fields are immutable. + val field = RcdField.source(key, value, false)(using rctx.owner) + lexicalBinders(tuple).foreach(binders => rctx.state.lexicalTypeBinders(field.tsym) = binders) + observeErasureDefinition(field.tsym.defn.get) + field)) record.withLocOf(tuple) // The record's fields are the tuple's own field expressions, so its // lexical scope is the tuple's. @@ -4537,14 +4892,8 @@ class NewResolver: primitive match case N => publish(N) case S(cls) => - val tpe = rctx.state.primitiveTypes.getOrElseUpdate(cls, { - val resolution = new TypeResolution(intro, messages => resolError(intro, messages)) - resolution.publish(TypeShape.Nominal(cls.defn.get)) - declaredType(resolution, Map.empty) - }) - listenTypeViews(tpe): - case nominal: NominalInstanceView => publish(S(nominal)) - case _ => softAssert(false, "Primitive class must expose a nominal interface") + val tpe = rctx.state.primitiveTypes.getOrElseUpdate(cls, primitiveType(cls, intro)) + listenPrimitiveInterface(tpe)(nominal => publish(S(nominal))) case Ref(sym) if sym is sym.getState.globalThisSymbol => listener(DynShape()) case SelfRef(sym) if sym is sym.getState.globalThisSymbol => listener(DynShape()) case ref @ Ref(loc: LocalSymbol) => diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolverState.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolverState.scala index 26152f71a0..c7b71f9880 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolverState.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolverState.scala @@ -69,6 +69,7 @@ object NewResolverState: final class NewResolverState(val owner: Elaborator.State): import NewResolverState.{Cache, Seen} + private[hkmc2] val erasureInputs = new ErasureInputs val context: ResolutionContext = new ResolutionContext(this, N, N, TypeSubstitution.empty) def isOwnedSym(symbol: Symbol): Bool = symbol.getState is owner private[semantics] val contexts = mutable.Map.empty[(ResolutionContext, TypeSubstitution), ResolutionContext] @@ -125,10 +126,20 @@ final class NewResolverState(val owner: Elaborator.State): new Cache(_.symShapes, identity) // Member `let`s have no TermDefinition body. Their initializers and subsequent // assignments feed this host, just as assignments feed a local variable. + // Assignable fields also have such a host, which holds only their assigned + // values (see NewResolver.writeField): their initializers are their bodies. // Keep the host in resolver state so imported observations use the existing // consumer-local publisher machinery rather than mutating the source symbol. private[semantics] val memberVariables: Cache[TermSymbol, ShapeHost] = new Cache(_.memberVariables, identity) + // An assignment's lookups can reach the same field through the same path from + // several receivers (see NewResolver.assignField); subscribe to its values once. + val fieldAssignments: Seen[(Identity[Term], Identity[Term], TermSymbol, Ls[Marks])] = + new Seen(_.fieldAssignments) + // The same assignment can also encounter several declared write interfaces. Retain each substituted + // input once per activation; callbacks assigned through it must satisfy that input, not the read type. + val fieldAssignmentTypes: Seen[(Identity[Term], Identity[Term], DeclaredType)] = + new Seen(_.fieldAssignmentTypes) val declaredSymShapes: Cache[(BlockMemberSymbol, FlowSymbol, Ls[Marks], Map[VarSymbol, DeclaredType], Bool), DeclaredSymShape] = new Cache(_.declaredSymShapes, identity) val selfShapes: Cache[InnerSymbol, BaseShape] = diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala index 3a29ec28fc..732c9d0a33 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Pattern.scala @@ -61,7 +61,7 @@ object Pattern: preallocated.get(name).foreach: expected => assert(symbols.forall(_ is expected), "Guard bindings must retain their extraction parameter symbol") // If no symbol had been created before, create a new symbol now. - val symbol = symbols.headOption.orElse(preallocated.get(name)).getOrElse(VarSymbol(Ident(name), erasedType = N)) + val symbol = symbols.headOption.orElse(preallocated.get(name)).getOrElse(VarSymbol(Ident(name))) aliases.foreach: alias => // For guarded patterns (`p where t`), the variables in `p` have to be // allocated before `t` is elaborated. In that case, we don't need to diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala index 92c125ea3b..40f41e2360 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala @@ -113,9 +113,9 @@ object ShapeIdentity: case view: NominalInstanceView => ValueKey.hash(System.identityHashCode(view.defn), ValueKey.mapHash(view.bindings)(System.identityHashCode, _.hashCode), parentHash(view.parent)) case view: CallableTypeShape => ValueKey.hash(System.identityHashCode(view.source), view.paramLists.hashCode, view.result.hashCode, view.scheme.hashCode, view.supplied.hashCode, - view.declaration.fold(0)(System.identityHashCode), view.asyncResult.hashCode) + view.declaration.fold(0)(System.identityHashCode), view.wrappedResult.hashCode) case view: RecordTypeShape => ValueKey.hash( - ValueKey.mapHash(view.fields)(_.hashCode, _.hashCode), view.aliases.hashCode) + ValueKey.mapHash(view.fields)(_.hashCode, _.hashCode), view.mutable.hashCode, view.aliases.hashCode) case view: IntersectionView => view.values.map(_.comparison).hashCode case view: SpecializedShape => ValueKey.hash(view.declaration.comparison.hashCode, view.arguments.hashCode, view.instances.hashCode) @@ -141,10 +141,10 @@ object ShapeIdentity: case a: CallableTypeShape => that.shape match case b: CallableTypeShape => (a.source eq b.source) && a.paramLists == b.paramLists && a.result == b.result && a.scheme == b.scheme && a.supplied == b.supplied && - a.declaration.fold(b.declaration.isEmpty)(x => b.declaration.exists(_ eq x)) && a.asyncResult == b.asyncResult + a.declaration.fold(b.declaration.isEmpty)(x => b.declaration.exists(_ eq x)) && a.wrappedResult == b.wrappedResult case _ => false case a: RecordTypeShape => that.shape match - case b: RecordTypeShape => a.fields == b.fields && a.aliases == b.aliases + case b: RecordTypeShape => a.fields == b.fields && a.mutable == b.mutable && a.aliases == b.aliases case _ => false case a: IntersectionView => that.shape match case b: IntersectionView => a.values.map(_.comparison) == b.values.map(_.comparison) @@ -462,7 +462,7 @@ enum MemberLookup: // type arguments; they never authorize reading an implementation's value flow. case Declared(member: BlockMemberSymbol, bindings: Map[VarSymbol, DeclaredType], marks: Ls[Marks], annotation: Opt[Term], positive: Bool) // A structural field already has its lexical bindings and intersection applied. - case Typed(field: RcdField, tpe: DeclaredType, marks: Ls[Marks]) + case Typed(field: RcdField, tpe: TypeArgument, marks: Ls[Marks]) // A preceding spread can place several different fields at the same index. case Indexed(fields: Ls[TupleShape.Fixed], marks: Ls[Marks]) case Dynamic(marks: Ls[Marks]) @@ -659,8 +659,12 @@ class BaseShape(val defn: ClassLikeDef, val ext: Opt[TermShape]) extends CoreHea * Field types are deferred references, including for recursive intersections. * Source declarations are witnesses for diagnostics and property selection, not * part of semantic equality: a merged field may have several such witnesses. + * Each field retains its read (output) and write (input) types separately: substitution can + * select different bounds for them, even when both were written as the same type variable. + * Immutable fields have Nothing as their input. The `mutable` fields (see RcdField) are + * part of the interface: values constrained by it admit their inputs as writes. */ -final class RecordTypeShape(val fields: Map[MemberKey, DeclaredType], val aliases: Map[Str, Set[SymbolKey]])( +final class RecordTypeShape(val fields: Map[MemberKey, TypeArgument], val mutable: Set[MemberKey], val aliases: Map[Str, Set[SymbolKey]])( val source: Term.Rcd, val declarations: Map[MemberKey, RcdField]) extends CoreHeadShape: def describe: Str = "record type" override def toString: Str = s"RecordTypeShape($fields)" @@ -817,15 +821,19 @@ final class DeclaredParams(val params: Ls[DeclaredParam], val variadic: Opt[Vari def mapTypes(f: DeclaredType => DeclaredType): DeclaredParams = DeclaredParams(params.map(_.mapType(f)), variadic.map(_.map(_.mapType(f)))) -/** Async lowering wraps the result only after all written parameter lists have - * been applied. A separate signature can encode those lists as nested arrows, +/** Async and generator lowering wrap the result only after all written parameter lists + * have been applied. A separate signature can encode those lists as nested arrows, * so the pending wrapper must survive observing the next callable interface. */ -final case class AsyncResult(annotation: Annot.Async, remainingLists: Int) extends ValueKey: - // Every async annotation wraps the result in Promise. Its span is evidence. - protected val valueHash: Int = ValueKey.hash(ValueKey.tag(this), remainingLists.hashCode) +final case class WrappedResult(annotation: Annot.Async | Annot.Generator, remainingLists: Int) extends ValueKey: + // Every async annotation wraps the result in Promise and every generator annotation + // in Iterator, so only the annotation's kind matters. Its span is evidence. + private def isAsync: Bool = annotation match + case _: Annot.Async => true + case _: Annot.Generator => false + protected val valueHash: Int = ValueKey.hash(ValueKey.tag(this), isAsync.hashCode, remainingLists.hashCode) protected def sameValue(other: ValueKey): Bool = other match - case that: AsyncResult => remainingLists == that.remainingLists + case that: WrappedResult => isAsync == that.isAsync && remainingLists == that.remainingLists case _ => false require(remainingLists > 0) @@ -839,7 +847,7 @@ final case class AsyncResult(annotation: Annot.Async, remainingLists: Int) exten */ final case class CallableTypeShape(source: Term, paramLists: NELs[DeclaredParams], result: Opt[DeclaredType], scheme: Opt[TypeScheme], supplied: Opt[Ls[DeclaredType]], - declaration: Opt[TermDefinition], asyncResult: Opt[AsyncResult]) extends CoreHeadShape: + declaration: Opt[TermDefinition], wrappedResult: Opt[WrappedResult]) extends CoreHeadShape: def tparams: Ls[DeclaredTypeParameter] = scheme.toList.flatMap(_.parameters) def describe: Str = declaration.fold("function with a declared signature")(d => s"function '${d.bsym.nme}'") def toLoc: Opt[Loc] = declaration.fold(source.toLoc)(_.headerLoc) @@ -852,6 +860,7 @@ final case class CallableTypeShape(source: Term, paramLists: NELs[DeclaredParams */ enum TypeInterfaceReason: case MissingOutput(source: Term) + case MissingFieldOutput(source: Term) case ContravariantParameter(parameter: TyParam, source: Term) case Unrestricted(source: Term) case AbstractDeclaration(symbol: TypeAliasSymbol, source: Term) @@ -860,10 +869,12 @@ enum TypeInterfaceReason: case Unavailable(source: Term) def provenance: ShapeProvenance = + def missingOutput(source: Term, subject: Str): Ls[(Message, Opt[Loc])] = + (msg"This $subject specifies only an input bound." -> source.toLoc) :: + (msg"Values read through it have no output bound more specific than 'Any'." -> N) :: Nil val result = ShapeProvenance(this match - case MissingOutput(source) => - (msg"This type argument specifies only an input bound." -> source.toLoc) :: - (msg"Values read through it have no output bound more specific than 'Any'." -> N) :: Nil + case MissingOutput(source) => missingOutput(source, "type argument") + case MissingFieldOutput(source) => missingOutput(source, "field") case ContravariantParameter(parameter, source) => (msg"This type argument supplies only an input bound for '${parameter.sym.nme}'." -> source.toLoc) :: (msg"Type parameter '${parameter.sym.nme}' is declared contravariant here." -> parameter.sym.toLoc) :: @@ -1229,8 +1240,10 @@ object TupleShape: /** A field found by record member lookup. Spreading r into `mut {...r}` reuses * its field symbols, but writes can change the copied values even if r is immutable. - * Store mutability on this lookup result: the mutable copy's initializer no longer - * determines its value shape, while reads from an immutable r can still use it. + * Store mutability on this lookup result: reads through the mutable copy also return + * the values assigned to the field, while reads from an immutable r can only return its + * initializer. Reads through any copy of r of a field declared `mut` (see RcdField) are mutable + * lookups too, as a copy reads the value that r holds when it is made. */ final case class RecordMember(field: RcdField, mutable: Bool) @@ -1251,7 +1264,7 @@ final class RecordShape private (val source: Term.Rcd, val elements: Ls[RecordSh case Nil => Missing case RecordShape.Field(field) :: rest => field.key match case S(key) => - if key == name then Found(RecordMember(field, source.mut), Nil) else loop(rest) + if key == name then Found(RecordMember(field, field.mut || source.mut), Nil) else loop(rest) case _ => unknown(field.field) case RecordShape.Unknown(shape) :: _ => Unknown(Uncertainty.RecordOverwrite, shape.fromPublicInterface, shape.provenance) case RecordShape.Dynamic(marks) :: _ => Dynamic(marks) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala index 6e4073aa4b..3b71147c16 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala @@ -242,11 +242,14 @@ sealed abstract class LocalVarSymbol(name: Str)(using State) extends FlowSymbol( var decl: Opt[Declaration] = N def subst(using s: SymbolSubst): LocalVarSymbol -/** A temporary variable introduced by lowering. +/** A temporary variable introduced by elaboration or lowering. * - * Its `erasedType` is once-mutable: a temp holding a branching term's result is named before those branches - * are lowered, so `Normalization` populates it with the join of their representations once they have all - * been seen. `Normalization.joinTempType` is its only write site. + * Its `erasedType` is once-mutable, and populated in two places: + * - A temp holding a branching term's result is named before those branches are lowered, so + * `Normalization.joinTempType` populates it with the join of their representations once they have all + * been seen. + * - A temp that elaboration declares with `LetDecl` (see `ErasureDeclarations`) is a slot like a source + * variable: `Erasure` populates it with the representation inferred from the values flowing into it. */ class TempSymbol(val trm: Opt[Term], override var erasedType: Opt[ErasedType], dbgNme: Str = "tmp")(using State) extends LocalVarSymbol(dbgNme) @@ -460,7 +463,7 @@ class ClassCtorSymbol( override val k: syntax.Fun.type, override val owner: Opt[InnerSymbol], val associatedCls: ClassSymbol, -)(using State) extends TermSymbol(k, owner, associatedCls.id, N): +)(using State) extends TermSymbol(k, owner, associatedCls.id): // Constructor functions occupy the class binding, not a private method slot. // MemberStorage determines that binding's runtime privacy from the class and module. // Their source accessibility is checked through the class and its constructor modifiers. diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index 8b9ce2bcf0..688774fa8e 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -750,8 +750,11 @@ final case class LetDecl(sym: LocalVarSymbol | TermSymbol, annotations: Ls[Annot /** The symbol identifies the property, independently of any local binding used * to evaluate its value. Computed keys also have an identity, but cannot be * selected statically until their key is known. Cloning preserves this identity. + * `mut` marks a field written `mut f: ...`, which can be assigned. Other fields are immutable, + * unless their whole record is mutable (`mut {...}`, see RecordMember); a record type's field + * marked `mut` can be assigned values of its type through the values that the type describes. */ -final case class RcdField(field: Term, rhs: Term, sym: BlockMemberSymbol)(val key: Opt[MemberKey]) extends Statement, AutoLocated: +final case class RcdField(field: Term, rhs: Term, sym: BlockMemberSymbol)(val mut: Bool, val key: Opt[MemberKey]) extends Statement, AutoLocated: // The two-argument RcdField.apply sets sym.tsym and its definition before // constructing this node. Lowering clones reuse the same initialized symbols. val tsym: TermSymbol = sym.tsym.get @@ -761,18 +764,20 @@ final case class RcdField(field: Term, rhs: Term, sym: BlockMemberSymbol)(val ke case _ => () object RcdField: - def apply(field: Term, rhs: Term)(using State): RcdField = make(field, rhs, false, N) - def signature(field: Term, sign: Term)(using State): RcdField = make(field, sign, true, N) - def signature(field: Term, sign: Term, symbol: SymbolKey)(using State): RcdField = - make(field, sign, true, S(symbol)) - private def make(field: Term, rhs: Term, signature: Bool, symbol: Opt[SymbolKey])(using State): RcdField = + // Lowering and pattern compilation synthesize immutable fields after source erasure. + def apply(field: Term, rhs: Term)(using State): RcdField = make(field, rhs, false, false, false, N) + def source(field: Term, rhs: Term, mut: Bool)(using State): RcdField = make(field, rhs, false, true, mut, N) + def signature(field: Term, sign: Term, mut: Bool, symbol: Opt[SymbolKey])(using State): RcdField = + make(field, sign, true, true, mut, symbol) + private def make(field: Term, rhs: Term, signature: Bool, source: Bool, mut: Bool, symbol: Opt[SymbolKey])(using State): RcdField = val name = field match case Term.Lit(Tree.StrLit(name)) => name case _ => "computed field" val id = new Tree.Ident(name) id.withLocOf(field) val sym = new BlockMemberSymbol(name, Nil) - val tsym = TermSymbol(RecordField, N, id, erasedType = N) + val tsym = TermSymbol(RecordField, N, id) + if !source then tsym.erasedType = N sym.tsym = S(tsym) // Value-field lookup removes the synthetic definition's capture mark, leaving // enclosing call-site marks intact. A structural type field instead refers @@ -781,7 +786,7 @@ object RcdField: if signature then S(rhs) else N, if signature then N else S(Term.Capture(rhs, tsym)), TermDefFlags.empty, Modulefulness.none, symbol.toList.map(Annot.JsSymbol(_)(field.toLoc)), N)(field.toLoc, field.toLoc, N)) sym.complete() - RcdField(field, rhs, sym)(symbol.orElse(field match + RcdField(field, rhs, sym)(mut, symbol.orElse(field match case Term.Lit(Tree.StrLit(name)) => S(name) case _ => N)) final case class RcdSpread(rcd: Term) extends Statement, AutoLocated @@ -876,6 +881,20 @@ final case class TermDefinition( if (k is syntax.Fun) && body.nonEmpty then annotations.collectFirst: case annotation: Annot.Async => annotation else N + /** A generator call returns a generator object; the body's result is only the payload of its + * final step. The elaborator rejects the annotation on functions without a parameter list, + * which are then compiled as ordinary functions. + */ + def generatorAnnotation: Opt[Annot.Generator] = + if (k is syntax.Fun) && body.nonEmpty && params.nonEmpty then annotations.collectFirst: + case annotation: Annot.Generator => annotation + else N + /** The annotation whose lowering replaces the function's result by a wrapper object + * (a promise or a generator) once all of its written parameter lists have been applied. + * Async generators are not modeled: a function with both annotations is treated as async. + */ + def resultWrapperAnnotation: Opt[Annot.Async | Annot.Generator] = + asyncAnnotation.orElse(generatorAnnotation) /** A result annotation denotes the whole result; a separate function signature * includes the arrows corresponding to the definition's written parameter lists. */ diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala index 3e572ddd5a..219e805020 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala @@ -123,7 +123,7 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: case d: Definition => ??? case imp: Import => Import(imp.sym, imp.str, imp.file)(imp.toLoc) case LetDecl(sym, annotations) => LetDecl(sym, annotations.map(_.mkClone))(toLoc) - case f @ RcdField(field, rhs, sym) => RcdField(field.mkClone, rhs.mkClone, sym)(f.key) + case f @ RcdField(field, rhs, sym) => RcdField(field.mkClone, rhs.mkClone, sym)(f.mut, f.key) case RcdSpread(rcd) => RcdSpread(rcd.mkClone) case DefineVar(sym, rhs) => DefineVar(sym, rhs.mkClone)(toLoc) case sc: SetConfig => sc @@ -430,7 +430,7 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: case Rcd(mut, stats) => (if mut then doc"mut " else doc"") :: braced: doc" # " :: stats.map(_.show).mkDocument(doc", # ") - case RcdField(field, rhs, _) => doc"${field.show}: ${rhs.show}" + case f @ RcdField(field, rhs, _) => doc"${if f.mut then "mut " else ""}${field.show}: ${rhs.show}" case RcdSpread(record) => doc"...${record.show}" case Quoted(body) => doc"""code"${body.show}"""" case Unquoted(body) => doc"$${${body.show}}" @@ -515,7 +515,7 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: case r @ SelfRef(sym) => sym.showAsPlain case Capture(base, thru) => s"${base.showDbg}^${thru.showDbg}" case App(lhs, rhs) => s"${lhs.showDbg}${rhs.showDbgAsParams}" - case RcdField(lhs, rhs, _) => s"${lhs.showDbg}: ${rhs.showDbg}" + case f @ RcdField(lhs, rhs, _) => s"${if f.mut then "mut " else ""}${lhs.showDbg}: ${rhs.showDbg}" case RcdSpread(bod) => s"...${bod.showDbg}" case FunTy(lhs: Tup, rhs, eff) => s"${lhs.fields.map(_.showDbg).mkString(", ")} ->${ diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/TypeShape.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/TypeShape.scala index 0387e4a471..4748f3f188 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/TypeShape.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/TypeShape.scala @@ -389,11 +389,12 @@ final class ContextualType(val tpe: DeclaredType, val marks: Ls[Marks]) extends case that: ContextualType => tpe == that.tpe && marks == that.marks case _ => false -/** An unqualified argument is interpreted separately in the two polarities; +/** An unqualified argument or mutable field is interpreted separately in the two polarities; * these parts coincide only when substitution gives the same type in both. * Missing wildcard parts are Nothing on input and Any on output, never holes. */ final class TypeArgument(val input: DeclaredType, val output: DeclaredType) extends ValueKey: + def map(f: DeclaredType => DeclaredType): TypeArgument = TypeArgument(f(input), f(output)) override def toString: Str = s"TypeArgument($input, $output)" protected val valueHash: Int = ValueKey.hash(ValueKey.tag(this), input.hashCode, output.hashCode) protected def sameValue(other: ValueKey): Bool = other match diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala index c6c76945e7..c16667a1cb 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala @@ -410,7 +410,7 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C // applied once both have been seen - here, before `cont` reads it. var pathTypes: Ls[Opt[codegen.ErasedValueType]] = Nil val exitCont: Result => Block = r => - pathTypes ::= r.erasedValueType + if !r.isUndefined then pathTypes ::= r.erasedValueType Assign(tmp, r, Break(exitLabel)) val bodyBlock = lowerSplit(sym.body, exitCont) val tailBlock = lowerSplit(tail, exitCont) @@ -503,7 +503,7 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C case IfLikeForm.ReturningIf => if (k is Ret) || (k is Thrw) then k(r) else - branchTypes ::= r.erasedValueType + if !r.isUndefined then branchTypes ::= r.erasedValueType Assign(l, r, End()) case IfLikeForm.ImperativeIf => Assign.discard(r, End()) case IfLikeForm.While => Assign(NoSymbol, r, loopCont) @@ -536,7 +536,11 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C // it, instead of forcing the branches through the top type and rejecting the read. if usesResTmp then joinTempType(l, branchTypes) // Embed the `body` into `Label` if the term is a `while`. - lazy val rest = if usesResTmp then k(l.asSimpleRef) else k(lowering.unit) + lazy val rest = + // The loop label is generated here and can only be continued, never + // broken to by source code. An abortive body therefore cannot reach k. + if form === IfLikeForm.While && body.isAbortive then End() + else if usesResTmp then k(l.asSimpleRef) else k(lowering.unit) val block = if form === IfLikeForm.While then // NOTE: `shouldRewriteWhile` is not the same as `config.rewriteWhileLoops` @@ -564,6 +568,10 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C blk.rest(rest) else Begin(Label(loopLabel, true, body, End()), rest) + else if body.isAbortive then + // Do not lower the continuation of an exhaustive returning split. + // Its unreachable result need not inhabit the function's representation. + body else if k.isInstanceOf[TailOp] && !form.isImperative // * ^ Generated imperative `if` branches do not always yield a value, so if we removed this, diff --git a/hkmc2/shared/src/test/mlscript-compile/Iter.mls b/hkmc2/shared/src/test/mlscript-compile/Iter.mls index dbba74ee32..853f19c107 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Iter.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Iter.mls @@ -27,8 +27,8 @@ open class IterableBase with else array // * Functions that create a fresh new result - open fun at(idx) = asArray.at(idx) - open fun concat(...xs) = asArray.concat(...xs) + open fun at(idx: Int): Any = asArray.at(idx) + open fun concat(...xs): Any = asArray.concat(...xs) fun copyWithin(...args) = asArray.copyWithin(...args) fun entries() = asArray.entries() fun every(...args) = asArray.every(...args) @@ -48,7 +48,7 @@ open class IterableBase with fun map(...args) = asArray.map(...args) fun reduce(...args) = asArray.reduce(...args) fun reduceRight(...args) = asArray.reduceRight(...args) - open fun slice(...args) = asArray.slice(...args) + open fun slice(...args): Any = asArray.slice(...args) fun some(...args) = asArray.some(...args) fun toLocaleString(...args) = asArray.toLocaleString(...args) fun toReversed() = asArray.toReversed() diff --git a/hkmc2/shared/src/test/mlscript-compile/ObjectBuffer.mls b/hkmc2/shared/src/test/mlscript-compile/ObjectBuffer.mls index 111802c2fd..860d456f19 100644 --- a/hkmc2/shared/src/test/mlscript-compile/ObjectBuffer.mls +++ b/hkmc2/shared/src/test/mlscript-compile/ObjectBuffer.mls @@ -7,9 +7,9 @@ module ObjectBuffer with abstract class ObjectBuffer with val buf fun mkNew[A](cls: { size: Int, ctor: (ObjectBuffer, Int) -> A }): A - fun del(cls: { size: Int }, inst) - fun read(idx) - fun write(idx, v) + fun del(cls: { size: Int }, inst: Int): Any + fun read(idx: Int): Any + fun write(idx: Int, v: Any): Any class DefaultObjectBuffer(initLength) extends ObjectBuffer with val buf = new mut Array(max(16, initLength)) diff --git a/hkmc2/shared/src/test/mlscript-compile/Predef.mjs b/hkmc2/shared/src/test/mlscript-compile/Predef.mjs index 2346028c93..eda31c15a9 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Predef.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Predef.mjs @@ -182,7 +182,7 @@ lambda = (undefined, function (Predef2, a, b, field) { } } static equals(a, b) { - let scrut, scrut1, scrut2, ac, scrut3, md, scrut4, scrut5, scrut6, scrut7, scrut8, scrut9, lambda$here; + let scrut, scrut1, scrut2, ac, scrut3, md, scrut4, scrut5, scrut6, scrut7, scrut8, scrut9, lambda$here, lambda$here1; scrut = a === b; if (scrut === true) { return true @@ -191,7 +191,8 @@ lambda = (undefined, function (Predef2, a, b, field) { if (b instanceof globalThis.Array) { scrut1 = a.length === b.length; if (scrut1 === true) { - return runtime.safeCall(a.every(lambda$1(Predef, b))) + lambda$here = lambda$1(Predef, b); + return runtime.safeCall(a.every(lambda$here)) } } } @@ -211,8 +212,8 @@ lambda = (undefined, function (Predef2, a, b, field) { md = ac[Predef.Symbols.definitionMetadata]; scrut4 = md !== undefined; if (scrut4 === true) { - lambda$here = lambda$(Predef, a, b); - scrut5 = runtime.safeCall(md[2].every(lambda$here)); + lambda$here1 = lambda$(Predef, a, b); + scrut5 = runtime.safeCall(md[2].every(lambda$here1)); if (scrut5 === true) { return true } diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs index e90d20c54a..007bf5c353 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs @@ -206,7 +206,9 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { } #_reified; resumeWith(value) { - return Runtime._try(lambda$(Runtime, this, value)) + let lambda$here; + lambda$here = lambda$(Runtime, this, value); + return Runtime._try(lambda$here) } raise() { Runtime.curEffect = this.reified; @@ -861,17 +863,18 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { return header; } static showFunctionContChain(cont, hl, vis, reps) { - let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, tmp4, scope31$cap; + let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, tmp4, scope31$cap, lambda$here; scope31$cap = new Capture$scope311(undefined); if (cont instanceof Runtime.FunctionContFrame.class) { tmp = cont["constructor"]["name"] + "(pc="; scope31$cap.result$0 = tmp + cont.saved.at(1); - runtime.safeCall(hl.forEach(lambda$2(scope31$cap, cont))); + lambda$here = lambda$2(scope31$cap, cont); + runtime.safeCall(hl.forEach(lambda$here)); scrut = runtime.safeCall(vis.has(cont)); if (scrut === true) { tmp1 = reps + 1; reps = tmp1; - scrut1 = reps > 10; + scrut1 = tmp1 > 10; if (scrut1 === true) { throw runtime.safeCall(globalThis.Error("10 repeated continuation frame (loop?)")) } @@ -891,16 +894,17 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { return "(NOT CONT)"; } static showHandlerContChain(cont, hl, vis, reps) { - let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, scope33$cap; + let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, scope33$cap, lambda$here; scope33$cap = new Capture$scope331(undefined); if (cont instanceof Runtime.HandlerContFrame.class) { scope33$cap.result$0 = cont.handler["constructor"]["name"]; - runtime.safeCall(hl.forEach(lambda$3(scope33$cap, cont))); + lambda$here = lambda$3(scope33$cap, cont); + runtime.safeCall(hl.forEach(lambda$here)); scrut = runtime.safeCall(vis.has(cont)); if (scrut === true) { tmp = reps + 1; reps = tmp; - scrut1 = reps > 10; + scrut1 = tmp > 10; if (scrut1 === true) { throw runtime.safeCall(globalThis.Error("10 repeated continuation frame (loop?)")) } @@ -1233,15 +1237,6 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { } return tmp } - static plus_impl(lhs, rhs) { - if (lhs instanceof Runtime.Int31.class) { - if (rhs instanceof Runtime.Int31.class) { - return lhs + rhs - } - return Runtime.unreachable; - } - return Runtime.unreachable; - } toString() { return runtime.render(this); } static [definitionMetadata] = ["class", "Runtime"]; }); diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mls b/hkmc2/shared/src/test/mlscript-compile/Runtime.mls index 7cf283f200..34694462cf 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mls @@ -144,8 +144,8 @@ fun printRaw(x) = console.log(render(x, indent: 2, breakLength: 76)) // TraceLogger module TraceLogger with - mut val enabled = false - mut val indentLvl = 0 + mut val enabled: Bool = false + mut val indentLvl: Int = 0 fun indent() = if enabled then let prev = indentLvl @@ -170,10 +170,10 @@ module TraceLogger with // Casts below decode slots whose types are fixed by this layout. mut val curEffect: EffectSig | null = null -mut val resumeValue = null -mut val resumeArr = null -mut val resumeIdx = null -mut val resumePc = -1 +mut val resumeValue: Any = null +mut val resumeArr: Array | null = null +mut val resumeIdx: Int | null = null +mut val resumePc: Int = -1 object FatalEffect object PrintStackEffect @@ -235,7 +235,7 @@ data class ContTrace( data class EffectSig(contTrace: ContTrace, handler: Any, handlerFun: Function | Bool | null) abstract class NonLocalReturn with - fun ret(value) + fun ret(value: Any): Any data class FnLocalsInfo(fnName: Str, locals: Array[LocalVarInfo]) data class LocalVarInfo(localName: Str, value: Any) @@ -511,10 +511,10 @@ fun toJsAsync(thunk: () -> Any) = Promise.resolve(r) // stack safety -mut val stackLimit = 0 // How deep the stack can go before heapifying the stack -mut val stackDepth = 0 -mut val stackHandler = null -mut val stackResume = null +mut val stackLimit: Int = 0 // How deep the stack can go before heapifying the stack +mut val stackDepth: Int = 0 +mut val stackHandler: Any = null +mut val stackResume: Function | null = null object StackDelayHandler with fun delay() = mkEffect of this, k => @@ -548,6 +548,3 @@ class Int31(v: Int) with fun zext() = bitand(v, bitnot(shl(1, 31))) as Int fun sext() = bitor(v, shl(1, 31)) as Int -fun plus_impl(lhs, rhs) = if lhs is - Int31 and rhs is Int31 then lhs + rhs - else unreachable diff --git a/hkmc2/shared/src/test/mlscript-compile/TreeTracer.mls b/hkmc2/shared/src/test/mlscript-compile/TreeTracer.mls index d620ea5248..9a02d3ce56 100644 --- a/hkmc2/shared/src/test/mlscript-compile/TreeTracer.mls +++ b/hkmc2/shared/src/test/mlscript-compile/TreeTracer.mls @@ -8,8 +8,8 @@ open Predef { fold } module TreeTracer with ... class TreeTracer with - mut val steps = 0 - mut val enabled = false + mut val steps: Int = 0 + mut val enabled: Bool = false fun output(outie, innie, innieAlt, message: Str) = if enabled do let lines = message.split("\n") lines.forEach of (line, i, xs) => if diff --git a/hkmc2/shared/src/test/mlscript-compile/apps/Accounting.mls b/hkmc2/shared/src/test/mlscript-compile/apps/Accounting.mls index 9267e1706f..6652c11ead 100644 --- a/hkmc2/shared/src/test/mlscript-compile/apps/Accounting.mls +++ b/hkmc2/shared/src/test/mlscript-compile/apps/Accounting.mls @@ -25,7 +25,7 @@ val mkProject = Project data class Line(val name: Str, val proj: Project, val starting_balance: Num, val isMatchable: Bool) with - mut val balance = starting_balance + mut val balance: Num = starting_balance fun expense(amt) = set balance = balance -. amt fun mustBeEmpty() = diff --git a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Parser.mls b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Parser.mls index 6c591c487a..9070658019 100644 --- a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Parser.mls +++ b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Parser.mls @@ -207,7 +207,7 @@ fun parse(tokens: Stack[Token.Token]) = consume let op = Tree.Ident(name, true) let rhs = expr(rightPrec, termOptions) - Tree.App(op, acc :: rhs :: Nil) exprCont(prec, options) + Tree.App(op, Tree.Tuple(acc :: rhs :: Nil)) exprCont(prec, options) else acc // * the case of application @@ -289,5 +289,5 @@ fun parse(tokens: Stack[Token.Token]) = token :: _ then let message = "expect EOF instead of " + token tracer.print of message, source.line - tree Tree.Error(message) + Tree.Error(Tree.Sequence(tree), message) :: Nil Nil then tree diff --git a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Rules.mls b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Rules.mls index 12b7b0a06b..70a42fa1b0 100644 --- a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Rules.mls +++ b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Rules.mls @@ -209,7 +209,7 @@ val termRule: ParseRule.ParseRule[Tree] = rule of Tree.Infix(Keywords._equalequal, lhs, rhs) // Comparison: * makeInfixChoice of Keywords._asterisk, "term", (lhs, rhs) => - Tree.App(Tree.Ident("*", true), lhs :: rhs :: Nil) + Tree.App(Tree.Ident("*", true), Tree.Tuple(lhs :: rhs :: Nil)) // Selection: "." // Access: "." "(" ")" keyword(Keywords._period) of diff --git a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Tree.mls b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Tree.mls index cccd9d063b..e97ed5f471 100644 --- a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Tree.mls +++ b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Tree.mls @@ -110,7 +110,7 @@ module Tree with "match " + go(scrutinee) + " with " branches Iter.fromStack() Iter.mapping(go) Iter.joined(" | ") // Function application for binary operators. - App(Ident(op, true), lhs :: rhs :: Nil) then + App(Ident(op, true), Tuple(lhs :: rhs :: Nil)) then if opPrec(op) is [leftPrec, rightPrec] then fold(+) of par of go(lhs), prec(lhs, false) < leftPrec " ", op, " " diff --git a/hkmc2/shared/src/test/mlscript-compile/nofib/fish.mls b/hkmc2/shared/src/test/mlscript-compile/nofib/fish.mls index 38d1f3bb8f..7ceca17fcb 100644 --- a/hkmc2/shared/src/test/mlscript-compile/nofib/fish.mls +++ b/hkmc2/shared/src/test/mlscript-compile/nofib/fish.mls @@ -153,7 +153,7 @@ fun fmt(ls) = if ls is showl(xs, s) nofibStringToList("[|") +: showFourTupleofInt(x) +: - showl(xs, "") + showl(xs, Nil) fun testFish_nofib(n) = map of diff --git a/hkmc2/shared/src/test/mlscript-compile/nofib/lambda.mls b/hkmc2/shared/src/test/mlscript-compile/nofib/lambda.mls index b0671c6a69..f7b7c7bed0 100644 --- a/hkmc2/shared/src/test/mlscript-compile/nofib/lambda.mls +++ b/hkmc2/shared/src/test/mlscript-compile/nofib/lambda.mls @@ -6,6 +6,9 @@ open NofibPrelude module lambda with ... +// The port uses Haskell strings, represented by linked lists of characters. +type String = List[Char] + fun lookup(k, t) = if t is diff --git a/hkmc2/shared/src/test/mlscript-compile/nofib/lastpiece.mls b/hkmc2/shared/src/test/mlscript-compile/nofib/lastpiece.mls index fb7e6a9cf3..0291f368f5 100644 --- a/hkmc2/shared/src/test/mlscript-compile/nofib/lastpiece.mls +++ b/hkmc2/shared/src/test/mlscript-compile/nofib/lastpiece.mls @@ -6,6 +6,9 @@ open NofibPrelude module lastpiece with ... +// The port uses Haskell strings, represented by linked lists of characters. +type String = List[Char] + fun isSome(x) = x is Some diff --git a/hkmc2/shared/src/test/mlscript/OverloadedModulesInSignatures.mls b/hkmc2/shared/src/test/mlscript/OverloadedModulesInSignatures.mls index 69bcf7a087..bcc78b9444 100644 --- a/hkmc2/shared/src/test/mlscript/OverloadedModulesInSignatures.mls +++ b/hkmc2/shared/src/test/mlscript/OverloadedModulesInSignatures.mls @@ -25,7 +25,9 @@ module TrmMod with :e fun f: ClsMod = 42 -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'ClsMod' +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'ClsMod' +//│ ║ l.27: fun f: ClsMod = 42 +//│ ╙── ^^ :rt f @@ -76,10 +78,12 @@ f :e fun f: TrmMod = 42 //│ ╔══[COMPILATION ERROR] Expected a non-module type; found reference of type TrmMod denoting module 'TrmMod'. -//│ ║ l.77: fun f: TrmMod = 42 +//│ ║ l.79: fun f: TrmMod = 42 //│ ║ ^^^^^^ //│ ╙── Function must be marked as returning a 'module' in order to have a module return type. -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'module TrmMod' +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'module TrmMod' +//│ ║ l.79: fun f: TrmMod = 42 +//│ ╙── ^^ :sjs fun f = TrmMod @@ -141,10 +145,10 @@ module Foo :e fun f(x: Foo): Foo = x //│ ╔══[COMPILATION ERROR] Expected 1 type arguments, got none -//│ ║ l.142: fun f(x: Foo): Foo = x +//│ ║ l.146: fun f(x: Foo): Foo = x //│ ╙── ^^^ //│ ╔══[COMPILATION ERROR] Expected 1 type arguments, got none -//│ ║ l.142: fun f(x: Foo): Foo = x +//│ ║ l.146: fun f(x: Foo): Foo = x //│ ╙── ^^^ fun f(x: Foo[Int]): Foo[Int] = x @@ -154,10 +158,10 @@ fun f(module x: Foo): module Foo = x :e fun f(module x: Foo[Int]): module Foo[Int] = x //│ ╔══[COMPILATION ERROR] Expected no type arguments, got 1 -//│ ║ l.155: fun f(module x: Foo[Int]): module Foo[Int] = x +//│ ║ l.159: fun f(module x: Foo[Int]): module Foo[Int] = x //│ ╙── ^^^^^^^^ //│ ╔══[COMPILATION ERROR] Expected no type arguments, got 1 -//│ ║ l.155: fun f(module x: Foo[Int]): module Foo[Int] = x +//│ ║ l.159: fun f(module x: Foo[Int]): module Foo[Int] = x //│ ╙── ^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/apps/parsing/LoopExpressions.mls b/hkmc2/shared/src/test/mlscript/apps/parsing/LoopExpressions.mls index 92c4349381..57da51ad32 100644 --- a/hkmc2/shared/src/test/mlscript/apps/parsing/LoopExpressions.mls +++ b/hkmc2/shared/src/test/mlscript/apps/parsing/LoopExpressions.mls @@ -23,7 +23,7 @@ done //│ > body = Some of While: //│ > condition = App: //│ > callee = Ident of "<" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > Ident of "x" //│ > Literal#Integer of "10" //│ > body = Sequence of Stack of @@ -38,7 +38,7 @@ done //│ > lhs = Ident of "x" //│ > rhs = App: //│ > callee = Ident of "+" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > Ident of "x" //│ > Literal#Integer of "1" //│ > «let x = 0 in while x < 10 do print_int x; print_newline (); x <- x + 1 done» diff --git a/hkmc2/shared/src/test/mlscript/apps/parsing/ParserErrorTest.mls b/hkmc2/shared/src/test/mlscript/apps/parsing/ParserErrorTest.mls index 8880ad29b3..857eb2ef1f 100644 --- a/hkmc2/shared/src/test/mlscript/apps/parsing/ParserErrorTest.mls +++ b/hkmc2/shared/src/test/mlscript/apps/parsing/ParserErrorTest.mls @@ -12,7 +12,7 @@ example of ":tree ((x) ==> x" example of ":tree ((x) ==> x))" //│ > App: //│ > callee = Ident of "==>" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > Ident of "x" //│ > Ident of "x" //│ > Error of "unexpected token Identifier(\")\", true)" @@ -61,16 +61,16 @@ example of """ """ //│ > App: //│ > callee = Ident of "*" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > App: //│ > callee = Ident of "*" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > App: //│ > callee = Ident of "-" //│ > argument = Literal#Integer of "2" //│ > App: //│ > callee = Ident of "~" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > Literal#Integer of "3" //│ > Literal#Integer of "2" //│ > Literal#Integer of "3" @@ -88,10 +88,10 @@ let foo = -2 * 3 //│ > lhs = Ident of "foo" //│ > rhs = App: //│ > callee = Ident of "*" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > App: //│ > callee = Ident of "*" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > App: //│ > callee = Ident of "-" //│ > argument = Literal#Integer of "2" diff --git a/hkmc2/shared/src/test/mlscript/apps/parsing/ParserTest.mls b/hkmc2/shared/src/test/mlscript/apps/parsing/ParserTest.mls index 932ab8e79a..8c7c33294f 100644 --- a/hkmc2/shared/src/test/mlscript/apps/parsing/ParserTest.mls +++ b/hkmc2/shared/src/test/mlscript/apps/parsing/ParserTest.mls @@ -59,7 +59,7 @@ let x: int -> int -> int = fun x y -> x + y //│ > argument = Ident of "y" //│ > body = App: //│ > callee = Ident of "+" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > Ident of "x" //│ > Ident of "y" //│ > body = None @@ -337,14 +337,14 @@ let x = !true """ //│ > App: //│ > callee = Ident of "*" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > App: //│ > callee = Ident of "-" //│ > argument = Literal#Integer of "2" //│ > Literal#Integer of "3" //│ > App: //│ > callee = Ident of "*" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > App: //│ > callee = Ident of "~" //│ > argument = Literal#Integer of "2" @@ -429,7 +429,7 @@ f -1 """ //│ > App: //│ > callee = Ident of "-" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > Ident of "f" //│ > Literal#Integer of "1" //│ > «f - 1» @@ -440,7 +440,7 @@ f :: -1 """ //│ > App: //│ > callee = Ident of "::" -//│ > argument = Stack of +//│ > argument = Tuple of Stack of //│ > Ident of "f" //│ > App: //│ > callee = Ident of "-" diff --git a/hkmc2/shared/src/test/mlscript/basics/BadMut.mls b/hkmc2/shared/src/test/mlscript/basics/BadMut.mls index c524c056ae..bc59d34a5c 100644 --- a/hkmc2/shared/src/test/mlscript/basics/BadMut.mls +++ b/hkmc2/shared/src/test/mlscript/basics/BadMut.mls @@ -9,17 +9,15 @@ mut "oops" //│ ╙── ^^^ //│ = "oops" -:e +// A record field can be declared mutable. mut a: 1 -//│ ╔══[COMPILATION ERROR] Unexpected record key shape. -//│ ║ l.13: mut a: 1 -//│ ╙── ^ -//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. +//│ = {a: 1} +//│ a = 1 :e mut {"oops"} //│ ╔══[COMPILATION ERROR] Expected a record after 'mut' keyword; found a block -//│ ║ l.20: mut {"oops"} +//│ ║ l.18: mut {"oops"} //│ ╙── ^^^^^^ //│ = "oops" diff --git a/hkmc2/shared/src/test/mlscript/basics/CallSyntaxes.mls b/hkmc2/shared/src/test/mlscript/basics/CallSyntaxes.mls index 5e48b7d8fa..a6d9156646 100644 --- a/hkmc2/shared/src/test/mlscript/basics/CallSyntaxes.mls +++ b/hkmc2/shared/src/test/mlscript/basics/CallSyntaxes.mls @@ -27,13 +27,17 @@ tuple(123, 456) // * Maybe later support this syntax? :pe :re +:ge 123 |> tuple(., 456) //│ ╔══[PARSE ERROR] Expected an expression; found period instead -//│ ║ l.30: 123 |> tuple(., 456) +//│ ║ l.31: 123 |> tuple(., 456) //│ ╙── ^ //│ ╔══[PARSE ERROR] Unexpected period here -//│ ║ l.30: 123 |> tuple(., 456) +//│ ║ l.31: 123 |> tuple(., 456) //│ ╙── ^ +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Array' at an unrelated type 'Function' +//│ ║ l.31: 123 |> tuple(., 456) +//│ ╙── ^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. diff --git a/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls b/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls index b038093965..02781f14aa 100644 --- a/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls +++ b/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls @@ -56,6 +56,9 @@ xs |. //│ ╟── Function 'map' expected 2 arguments, but got 1 //│ ║ l.42: fun map(xs: Array, f) = xs.map((x, ...bs) => f(x)) //│ ╙── ^^^^^^^^^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Function' at an unrelated type 'Array' +//│ ║ l.52: map(x => x * 2) +//│ ╙── ^^^^^^^^^^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let lambda5, tmp; //│ lambda5 = (undefined, function (x) { @@ -71,11 +74,14 @@ xs |. xs|. map(x => x * 2) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.72: map(x => x * 2) +//│ ║ l.75: map(x => x * 2) //│ ║ ^^^^^^^^^^^^^^^ //│ ╟── Function 'map' expected 2 arguments, but got 1 //│ ║ l.42: fun map(xs: Array, f) = xs.map((x, ...bs) => f(x)) //│ ╙── ^^^^^^^^^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Function' at an unrelated type 'Array' +//│ ║ l.75: map(x => x * 2) +//│ ╙── ^^^^^^^^^^ //│ ═══[RUNTIME ERROR] Error: Function 'map' expected at least 2 arguments but got 1 :pt diff --git a/hkmc2/shared/src/test/mlscript/basics/MultiParamListClasses.mls b/hkmc2/shared/src/test/mlscript/basics/MultiParamListClasses.mls index 2d2384972e..fa8985b063 100644 --- a/hkmc2/shared/src/test/mlscript/basics/MultiParamListClasses.mls +++ b/hkmc2/shared/src/test/mlscript/basics/MultiParamListClasses.mls @@ -85,24 +85,16 @@ Foo(1, 2) :sjs let f = Foo // should compile to `(x, y) => Foo(x, y)(use[Int])` //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let f2, f3; -//│ f3 = function f(x1, y1) { -//│ let x2, y2, z2, tmp2, lambda4, f4; -//│ x2 = x1; -//│ y2 = y1; -//│ z2 = instance$Ident$_Int$_; +//│ let f, f1; +//│ f1 = function f(x, y) { +//│ let tmp2; //│ tmp2 = new Foo03(); -//│ lambda4 = (undefined, function (res) { -//│ res.x = x2; -//│ res.y = y2; -//│ res.z = z2; -//│ return runtime.Unit -//│ }); -//│ f4 = lambda4; -//│ runtime.safeCall(f4(tmp2)); +//│ tmp2.x = x; +//│ tmp2.y = y; +//│ tmp2.z = instance$Ident$_Int$_; //│ return tmp2 //│ }; -//│ f2 = f3; +//│ f = f1; //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ f = fun f diff --git a/hkmc2/shared/src/test/mlscript/basics/PartialApps.mls b/hkmc2/shared/src/test/mlscript/basics/PartialApps.mls index 8e1ca50f75..069981cff7 100644 --- a/hkmc2/shared/src/test/mlscript/basics/PartialApps.mls +++ b/hkmc2/shared/src/test/mlscript/basics/PartialApps.mls @@ -160,7 +160,15 @@ f(1) let f = => add of _, 1 //│ f = fun f +// The sum computed by `add` now also reaches the erroneous `f` defined above, which this call reports. +:e f(1) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.144: f(1) +//│ ║ ^^^^ +//│ ╟── Value of type 'Int' cannot be called like a function. +//│ ║ l.40: fun add = => _ + _ +//│ ╙── ^^^^^ //│ = 2 diff --git a/hkmc2/shared/src/test/mlscript/basics/ShortcircuitingOps.mls b/hkmc2/shared/src/test/mlscript/basics/ShortcircuitingOps.mls index f1e20720e2..ba30a812b5 100644 --- a/hkmc2/shared/src/test/mlscript/basics/ShortcircuitingOps.mls +++ b/hkmc2/shared/src/test/mlscript/basics/ShortcircuitingOps.mls @@ -11,7 +11,7 @@ fun - loud(x) = print(x); x + loud(x: Any) = print(x); x loudTrue = loud(true) loudFalse = loud(false) diff --git a/hkmc2/shared/src/test/mlscript/block-staging/Casts.mls b/hkmc2/shared/src/test/mlscript/block-staging/Casts.mls index 31b8da7bfc..54bcd8c3ad 100644 --- a/hkmc2/shared/src/test/mlscript/block-staging/Casts.mls +++ b/hkmc2/shared/src/test/mlscript/block-staging/Casts.mls @@ -8,7 +8,7 @@ :js :staging class Top(x: Int) -fun anything(v) = v +fun anything(v: Any): Any = v staged module M with fun mk(): Top = anything(1) //│ > fun ctor_() = () diff --git a/hkmc2/shared/src/test/mlscript/block-staging/PrintCode.mls b/hkmc2/shared/src/test/mlscript/block-staging/PrintCode.mls index 780ad22c8e..b66d27467b 100644 --- a/hkmc2/shared/src/test/mlscript/block-staging/PrintCode.mls +++ b/hkmc2/shared/src/test/mlscript/block-staging/PrintCode.mls @@ -34,7 +34,7 @@ printCode(DynSelect(ValueSimpleRef(Symbol("p")), ValueSimpleRef(Symbol("field")) :silent class A(val x)(y) class B -let aSym = ConcreteClassSymbol("A", A, Some([Symbol("x")]), [[Symbol("y")]]) +let aSym = ConcreteClassSymbol("A", A.class, Some([Symbol("x")]), [[Symbol("y")]]) let bSym = ConcreteClassSymbol("B", B, None, []) printCode(Call(ValueSimpleRef(Symbol("f")), [Arg(ValueLit(3))])) diff --git a/hkmc2/shared/src/test/mlscript/codegen/CaseShorthand.mls b/hkmc2/shared/src/test/mlscript/codegen/CaseShorthand.mls index 66020f8c1a..fefa8bca1b 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/CaseShorthand.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/CaseShorthand.mls @@ -90,12 +90,12 @@ case 0 then true _ then false //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let lambda10; -//│ lambda10 = (undefined, function (caseScrut) { +//│ let lambda5; +//│ lambda5 = (undefined, function (caseScrut) { //│ if (caseScrut === 0) { return true } //│ return false; //│ }); -//│ return lambda10 +//│ return lambda5 //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = fun @@ -107,8 +107,8 @@ val isDefined = case Some then true None then false //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let isDefined, lambda11; -//│ lambda11 = (undefined, function (caseScrut) { +//│ let isDefined, lambda6; +//│ lambda6 = (undefined, function (caseScrut) { //│ if (caseScrut instanceof Some1.class) { //│ return true //│ } else if (caseScrut instanceof None1.class) { @@ -116,7 +116,7 @@ val isDefined = case //│ } //│ throw globalThis.Object.freeze(new globalThis.Error("match error")); //│ }); -//│ isDefined = lambda11; +//│ isDefined = lambda6; //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ isDefined = fun diff --git a/hkmc2/shared/src/test/mlscript/codegen/CheckedCasts.mls b/hkmc2/shared/src/test/mlscript/codegen/CheckedCasts.mls index c2fac25dc9..3ebc2350f8 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/CheckedCasts.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/CheckedCasts.mls @@ -5,12 +5,12 @@ // checked, `as!!` when not. :sir -fun checked(x: Int, y: Int): Int = x + y +fun checked(x: Num, y: Int): Int = x + y //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define checked⁰ as fun checked¹(x, y) { return (+⁰(x, y) as! Int⁰) }; end :noCheckCasts :sir -fun unchecked(x: Int, y: Int): Int = x + y +fun unchecked(x: Num, y: Int): Int = x + y //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define unchecked⁰ as fun unchecked¹(x, y) { return (+⁰(x, y) as!! Int⁰) }; end diff --git a/hkmc2/shared/src/test/mlscript/codegen/ErasedTypes.mls b/hkmc2/shared/src/test/mlscript/codegen/ErasedTypes.mls index 75cbeb1fe9..81f76e8c6b 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ErasedTypes.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ErasedTypes.mls @@ -11,8 +11,7 @@ open annotations //│ end -// Erased types come only from explicit type annotations. An unannotated local - even one bound to a -// literal - carries no erased type. +// Local bindings infer representations from their initializers, including primitive literals and aggregates. fun main = let i = 1 let f = 1.5 @@ -21,8 +20,8 @@ fun main = let t = [i, s] let r = {x: i, y: s} //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define main⁰ as fun main¹(): ? { -//│ let i: ?, f: ?, s: ?, b: ?, t: ?, r: ?, x: ?, y: ?; +//│ define main⁰ as fun main¹(): Unit⁰ { +//│ let i: Int⁰, f: Num⁰, s: Str⁰, b: Bool⁰, t: Array⁰, r: Unknown, x: Int⁰, y: Str⁰; //│ set i = 1; //│ set f = 1.5; //│ set s = "str"; @@ -36,8 +35,8 @@ fun main = //│ end -// An annotated slot does carry one, whatever binds it: each `val` below is typed from its annotation, and -// so is the temporary the array literal spills into. +// Explicit annotations fix the representations of these `val` slots. The temporary holding the array literal +// also carries the array representation. val i: Int = 1 val f: Num = 1.5 val s: Str = "str" @@ -144,8 +143,8 @@ fun main = let foo = new Foo(123) let bar = new Bar("hi") //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define main² as fun main³(): ? { -//│ let foo: ?, bar: ?; +//│ define main² as fun main³(): Unit⁰ { +//│ let foo: Foo⁰, bar: Bar⁰; //│ set foo = new Foo⁰(123); //│ set bar = new Bar⁰("hi"); //│ return runtime⁰.Unit⁰ @@ -165,17 +164,17 @@ fun main = //│ end -// An unannotated parameter is `?`, so the call may pass anything; the narrowing lands where the value reaches -// a typed slot instead - here on the constructor argument. -fun mk(a) = new Bar(a) +// An explicit `Any` parameter admits values of any reference representation. Narrowing to `Str` happens +// at the constructor argument, even when the only local call passes an integer. +fun mk(a: Any) = new Bar(a) fun main = let made = mk(1) made.y //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define mk⁰ as fun mk¹(a: ?): ? { +//│ define mk⁰ as fun mk¹(a: Unknown): Bar⁰ { //│ return new Bar⁰((a as! Str⁰)) //│ }; -//│ define main⁴ as fun main⁵(): ? { let made: ?; set made = mk¹(1); return made.y¹ }; +//│ define main⁴ as fun main⁵(): Str⁰ { let made: Bar⁰; set made = mk¹(1); return made.y¹ }; //│ end @@ -183,20 +182,22 @@ fun main = // than a cast. The definition is still lowered - uncast - so the blocks after it keep compiling. :ge fun test(x: Int): Int = "oops" -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ║ l.184: fun test(x: Int): Int = "oops" +//│ ╙── ^^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define test⁰ as fun test¹(x: Int⁰): Int⁰ { return "oops" }; end -// The return coercion is what an annotated result buys. `(+)` is a builtin and carries no erased type, so +// The return coercion is what an annotated result buys. The sum of two `Num`s is a `Num`, so // `add2` narrows its result to `Int` while `add1`, having nothing to narrow to, leaves it alone. -fun add1(x: Int, y: Int) = x + y -fun add2(x: Int, y: Int): Int = x + y +fun add1(x: Num, y: Num) = x + y +fun add2(x: Num, y: Num): Int = x + y //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define add1⁰ as fun add1¹(x: Int⁰, y: Int⁰): ? { +//│ define add1⁰ as fun add1¹(x: Num⁰, y: Num⁰): Num⁰ { //│ return +⁰(x, y) //│ }; -//│ define add2⁰ as fun add2¹(x: Int⁰, y: Int⁰): Int⁰ { return (+⁰(x, y) as! Int⁰) }; +//│ define add2⁰ as fun add2¹(x: Num⁰, y: Num⁰): Int⁰ { return (+⁰(x, y) as! Int⁰) }; //│ end @@ -205,21 +206,21 @@ fun keep(x: Int) = set x = 5 x //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define keep⁰ as fun keep¹(x: Int⁰): ? { set x = 5; return x }; end +//│ define keep⁰ as fun keep¹(x: Int⁰): Int⁰ { set x = 5; return x }; end // One class, four facets of the same rule: // -// - `put` - a `mut val` field keeps its annotation, so a write through an unannotated parameter narrows at -// the assignment rather than at the call; +// - `put` - a `mut val` field keeps its annotation, so a write from an unknown input narrows at +// the assignment; // - `get` - reading the field back needs no cast, since the selection carries the member's type. The result // annotation is what makes that observable: without it the golden is identical either way; // - `twice` - an annotated method result narrows the `(+)` call, exactly as the top-level `add2` does above; -// - `whatever` - an unannotated one has nothing to narrow to. +// - `whatever` - forwarding an input imposes no additional representation annotation. class Cell(mut val v: Int) with fun put(x) = set v = x fun get(): Int = v - fun twice(x: Int): Int = x + x + fun twice(x: Num): Int = x + x fun whatever(a) = a //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define Cell⁰ as class Cell² { @@ -228,14 +229,14 @@ class Cell(mut val v: Int) with //│ define v² as val v³: Int⁰ = v; //│ end //│ } -//│ method put⁰ = fun put¹(x: ?): ? { +//│ method put⁰ = fun put¹(x: ?): Unit⁰ { //│ set Cell².this.v³ = (x as! Int⁰); //│ return runtime⁰.Unit⁰ //│ } //│ method get⁰ = fun get¹(): Int⁰ { //│ return Cell².this.v³ //│ } -//│ method twice⁰ = fun twice¹(x: Int⁰): Int⁰ { +//│ method twice⁰ = fun twice¹(x: Num⁰): Int⁰ { //│ return (+⁰(x, x) as! Int⁰) //│ } //│ method whatever⁰ = fun whatever¹(a: ?): ? { return a } @@ -243,14 +244,14 @@ class Cell(mut val v: Int) with //│ end -// A rest parameter has no annotation to erase, so both it and the list it binds stay `?`. +// Record the parameter and result representations of an unannotated variadic function. fun rest(a, ...xs) = xs //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define rest⁰ as fun rest¹(a: ?, ...xs: ?): ? { return xs }; end +//│ define rest⁰ as fun rest¹(a: ?, ...xs: Array⁰): Array⁰ { return xs }; end -// Closure capture (`:lift`): the captured local `n` is unannotated, so the generated capture class and its -// field have no type to inherit and stay `: ?`. The next block is the same shape with an annotated local. +// Closure capture (`:lift`) preserves the inferred representation of the mutable local `n` in its generated +// capture field. The next block checks the corresponding case for an annotated parameter. :lift fun counter(x: Int) = let n = 0 @@ -260,19 +261,19 @@ fun counter(x: Int) = //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let bump, Capture$scope0; //│ define Capture$scope0 as class Capture$scope0¹ { -//│ constructor Capture$scope0⁰(n$0: ?) { -//│ define n$0⁰ as val n$0¹: ? = n$0; +//│ constructor Capture$scope0⁰(n$0: Int⁰) { +//│ define n$0⁰ as val n$0¹: Int⁰ = n$0; //│ end //│ } //│ }; -//│ define bump as fun bump⁰(scope0$cap: Capture$scope0¹, x: Int⁰): ? { -//│ let tmp: ?; +//│ define bump as fun bump⁰(scope0$cap: Capture$scope0¹, x: Int⁰): Unit⁰ { +//│ let tmp: Int⁰; //│ set tmp = +⁰(scope0$cap.n$0¹, x); //│ set scope0$cap.n$0¹ = tmp; //│ return runtime⁰.Unit⁰ //│ }; -//│ define counter⁰ as fun counter¹(x: Int⁰): ? { -//│ let n: ?, scope0$cap: Capture$scope0¹; +//│ define counter⁰ as fun counter¹(x: Int⁰): Int⁰ { +//│ let n: Int⁰, scope0$cap: Capture$scope0¹; //│ set scope0$cap = new mut Capture$scope0¹(n); //│ set scope0$cap.n$0¹ = 0; //│ do bump⁰(scope0$cap, x); @@ -284,7 +285,7 @@ fun counter(x: Int) = // When the captured slot keeps a definite erased type (the parameter `x` is annotated `Int`), // the generated capture field inherits it: both the constructor parameter `x$0` and the // `val x$0` field are typed `Int`, and the auxiliary parameter of the lifted function is -// typed as well. Without this, the capture class is untyped and the field reads are `?`. +// typed as well. Losing these types would introduce casts when the captured value is read back. :lift fun resetCounter(x: Int): Int = fun reset() = set x = x + 1 @@ -293,10 +294,10 @@ fun resetCounter(x: Int): Int = resetCounter(41) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let reset, Capture$resetCounter; -//│ define reset as fun reset⁰(resetCounter$cap: Capture$resetCounter⁰): ? { -//│ let tmp: ?; +//│ define reset as fun reset⁰(resetCounter$cap: Capture$resetCounter⁰): Unit⁰ { +//│ let tmp: Int⁰; //│ set tmp = +⁰(resetCounter$cap.x$0⁰, 1); -//│ set resetCounter$cap.x$0⁰ = (tmp as! Int⁰); +//│ set resetCounter$cap.x$0⁰ = tmp; //│ return runtime⁰.Unit⁰ //│ }; //│ define Capture$resetCounter as class Capture$resetCounter⁰ { @@ -316,9 +317,7 @@ resetCounter(41) // `Int` and `Int32` are both represented as host primitives, so both are *siblings* of `Object` rather than // descendants of it: neither direction is a widening or a narrowing, and all four crossings below are -// rejected. What separates the two is the fifth error: an `Int` is boxed and still reaches the top type, -// while an `Int32` cannot - so returning `Int32` from `f`, whose result is unannotated and therefore -// `Unknown`, is rejected where returning an `Int` would not be. +// rejected. The unannotated result of `f` can retain the `Int32` representation of its final call. :ge fun useObj(o: Object) = o fun useInt(n: Int) = n @@ -329,29 +328,28 @@ fun f(o: Object, i: Int, x: Int32) = useObj(x) useInt32(o) //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Object' -//│ ║ l.327: useObj(i) +//│ ║ l.326: useObj(i) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Object' at an unrelated type 'Int' -//│ ║ l.328: useInt(o) +//│ ║ l.327: useInt(o) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int32' at an unrelated type 'Object' -//│ ║ l.329: useObj(x) +//│ ║ l.328: useObj(x) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Object' at an unrelated type 'Int32' -//│ ║ l.330: useInt32(o) +//│ ║ l.329: useInt32(o) //│ ╙── ^ -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Int32' at an unrelated type 'Unknown' //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define useObj⁰ as fun useObj¹(o: Object⁰): ? { +//│ define useObj⁰ as fun useObj¹(o: Object⁰): Object⁰ { //│ return o //│ }; -//│ define useInt⁰ as fun useInt¹(n: Int⁰): ? { +//│ define useInt⁰ as fun useInt¹(n: Int⁰): Int⁰ { //│ return n //│ }; //│ define useInt32⁰ as fun useInt32¹(n: Int32): Int32 { //│ return n //│ }; -//│ define f² as fun f³(o: Object⁰, i: Int⁰, x: Int32): ? { +//│ define f² as fun f³(o: Object⁰, i: Int⁰, x: Int32): Int32 { //│ do useObj¹(i); //│ do useInt¹(o); //│ do useObj¹(x); @@ -365,13 +363,13 @@ fun f(o: Object, i: Int, x: Int32) = fun id(x: Anything) = () fun f(x: Int32) = id(x) //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int32' at an unrelated type 'Unknown' -//│ ║ l.366: fun f(x: Int32) = id(x) +//│ ║ l.364: fun f(x: Int32) = id(x) //│ ╙── ^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define id⁰ as fun id¹(x: Unknown): ? { +//│ define id⁰ as fun id¹(x: Unknown): Unit⁰ { //│ return runtime⁰.Unit⁰ //│ }; -//│ define f⁴ as fun f⁵(x: Int32): ? { return id¹(x) }; +//│ define f⁴ as fun f⁵(x: Int32): Unit⁰ { return id¹(x) }; //│ end @@ -382,10 +380,10 @@ fun f1(x: Int31) = id(x) fun f2(x: Int) = id(x) fun f3(x: Anything): Anything = x //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define f1⁰ as fun f1¹(x: Int31⁰): ? { +//│ define f1⁰ as fun f1¹(x: Int31⁰): Unit⁰ { //│ return id¹(x) //│ }; -//│ define f2⁰ as fun f2¹(x: Int⁰): ? { +//│ define f2⁰ as fun f2¹(x: Int⁰): Unit⁰ { //│ return id¹(x) //│ }; //│ define f3⁰ as fun f3¹(x: Unknown): Unknown { return x }; @@ -402,10 +400,10 @@ module Encl with class Shape fun bad(x: Shape) = useShape(x) //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Encl.Shape' at an unrelated type 'Shape' -//│ ║ l.403: fun bad(x: Shape) = useShape(x) +//│ ║ l.401: fun bad(x: Shape) = useShape(x) //│ ╙── ^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define useShape⁰ as fun useShape¹(x: Shape⁰): ? { +//│ define useShape⁰ as fun useShape¹(x: Shape⁰): Shape⁰ { //│ return x //│ }; //│ define Shape¹ as class Shape⁰; @@ -415,7 +413,7 @@ module Encl with //│ define Shape² as class Shape³; //│ end //│ } -//│ method bad⁰ = fun bad¹(x: Shape³): ? { return useShape¹(x) } +//│ method bad⁰ = fun bad¹(x: Shape³): Shape⁰ { return useShape¹(x) } //│ }; //│ end @@ -426,10 +424,10 @@ fun g(i: Int, i31: Int31) = useInt(i31) useInt31(i) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define useInt31⁰ as fun useInt31¹(n: Int31⁰): ? { +//│ define useInt31⁰ as fun useInt31¹(n: Int31⁰): Int31⁰ { //│ return n //│ }; -//│ define g⁰ as fun g¹(i: Int⁰, i31: Int31⁰): ? { +//│ define g⁰ as fun g¹(i: Int⁰, i31: Int31⁰): Int31⁰ { //│ do useInt¹(i31); //│ return useInt31¹((i as! Int31⁰)) //│ }; @@ -488,13 +486,13 @@ type Union = P | Q fun useCyclic(x: Cyclic): Cyclic = x fun useUnion(x: Union): Union = x //│ ╔══[COMPILATION ERROR] Resolution error in member reference -//│ ║ l.484: type Cyclic = Cyclic +//│ ║ l.482: type Cyclic = Cyclic //│ ║ ^^^^^^ //│ ╟── Unguarded recursive type alias 'Cyclic'. -//│ ║ l.484: type Cyclic = Cyclic +//│ ║ l.482: type Cyclic = Cyclic //│ ║ ^^^^^^ //│ ╟── Recursive references must pass through a record, tuple, function, or nominal type. -//│ ║ l.484: type Cyclic = Cyclic +//│ ║ l.482: type Cyclic = Cyclic //│ ╙── ^^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define useCyclic⁰ as fun useCyclic¹(x: Unknown): Unknown { @@ -534,16 +532,16 @@ fun unionIntoSubtype(x: A | B) = takesSubtype(x) //│ define takesSubtype⁰ as fun takesSubtype¹(a: A⁰): Unit⁰ { //│ return runtime⁰.Unit⁰ //│ }; -//│ define unionIntoBase⁰ as fun unionIntoBase¹(x: Base⁰): ? { +//│ define unionIntoBase⁰ as fun unionIntoBase¹(x: Base⁰): Unknown { //│ return takesBase¹(x) //│ }; -//│ define memberIntoUnion⁰ as fun memberIntoUnion¹(a: A⁰): ? { +//│ define memberIntoUnion⁰ as fun memberIntoUnion¹(a: A⁰): Unknown { //│ return takesUnion¹(a) //│ }; -//│ define baseIntoUnion⁰ as fun baseIntoUnion¹(b: Base⁰): ? { +//│ define baseIntoUnion⁰ as fun baseIntoUnion¹(b: Base⁰): Unknown { //│ return takesUnion¹(b) //│ }; -//│ define unionIntoSubtype⁰ as fun unionIntoSubtype¹(x: Base⁰): ? { +//│ define unionIntoSubtype⁰ as fun unionIntoSubtype¹(x: Base⁰): Unknown { //│ return takesSubtype¹((x as! A⁰)) //│ }; //│ define Base¹ as class Base⁰; @@ -560,13 +558,13 @@ fun mixed(x: Int32 | Str) = 0 fun twoPrims(x: Int32 | Int64) = 0 fun mixedThree(x: Int32 | Str | Bool) = 0 //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define mixed⁰ as fun mixed¹(x: ‹incompatible(Int32, Str⁰)›): ? { +//│ define mixed⁰ as fun mixed¹(x: ‹incompatible(Int32, Str⁰)›): Int⁰ { //│ return 0 //│ }; -//│ define twoPrims⁰ as fun twoPrims¹(x: ‹incompatible(Int32, Int64)›): ? { +//│ define twoPrims⁰ as fun twoPrims¹(x: ‹incompatible(Int32, Int64)›): Int⁰ { //│ return 0 //│ }; -//│ define mixedThree⁰ as fun mixedThree¹(x: ‹incompatible(Int32, Str⁰)›): ? { return 0 }; +//│ define mixedThree⁰ as fun mixedThree¹(x: ‹incompatible(Int32, Str⁰)›): Int⁰ { return 0 }; //│ end @@ -575,10 +573,10 @@ fun mixedThree(x: Int32 | Str | Bool) = 0 :ge fun intoMixed(n: Int32) = mixed(n) //│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation -//│ ║ l.576: fun intoMixed(n: Int32) = mixed(n) +//│ ║ l.574: fun intoMixed(n: Int32) = mixed(n) //│ ╙── ^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define intoMixed⁰ as fun intoMixed¹(n: Int32): ? { return mixed¹(n) }; end +//│ define intoMixed⁰ as fun intoMixed¹(n: Int32): Int⁰ { return mixed¹(n) }; end // Widening fails too: an unrepresentable value cannot enter an `Object` slot either. Both errors are @@ -586,8 +584,12 @@ fun intoMixed(n: Int32) = mixed(n) :ge fun outOfMixed(x: Int32 | Str): Str = x fun mixedIntoObject(x: Int32 | Str): Object = x -//│ ═══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation -//│ ═══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.585: fun outOfMixed(x: Int32 | Str): Str = x +//│ ╙── ^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.586: fun mixedIntoObject(x: Int32 | Str): Object = x +//│ ╙── ^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define outOfMixed⁰ as fun outOfMixed¹(x: ‹incompatible(Int32, Str⁰)›): Str⁰ { //│ return x @@ -602,16 +604,12 @@ fun mixedIntoObject(x: Int32 | Str): Object = x // through `sccG`'s arm with a `Num`, so the cast is deferred to the exit. :soir fun sccF(): Int = sccG() -fun sccG(): Num = sccF() + 1 +fun sccG(): Num = sccF() + 0.5 //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define sccF⁰ as fun sccF¹(): Int⁰ { //│ return (sccG⁰() as! Int⁰) //│ }; -//│ define sccG¹ as fun sccG⁰(): Num⁰ { -//│ let tmp: Int⁰; -//│ set tmp = sccF¹(); -//│ return (+⁰(tmp, 1) as! Num⁰) -//│ }; +//│ define sccG¹ as fun sccG⁰(): Num⁰ { let tmp: Int⁰; set tmp = sccF¹(); return +⁰(tmp, 0.5) }; //│ end //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let sccF_sccG; @@ -628,7 +626,7 @@ fun sccG(): Num = sccF() + 1 //│ 1 => //│ let tmp: Int⁰, exitResult: Num⁰; //│ set tmp = (sccF_sccG⁰(0) as! Int⁰); -//│ set exitResult = (+⁰(tmp, 1) as! Num⁰); +//│ set exitResult = +⁰(tmp, 0.5); //│ match sccF$deferred //│ true => //│ return (exitResult as! Int⁰) @@ -661,36 +659,36 @@ annotHelper(100000) //│ define annotLoop⁰ as fun annotLoop¹(n: Int⁰): Unit⁰ { //│ return (annotHelper⁰(n) as! Unit⁰) //│ }; -//│ define annotHelper¹ as fun annotHelper⁰(n: ?): ? { -//│ let scrut: ?, tmp: ?; +//│ define annotHelper¹ as fun annotHelper⁰(n: Int⁰): Unknown { +//│ let scrut: ?, tmp: Int⁰; //│ set scrut = <=⁰(n, 0); //│ match scrut //│ true => //│ return runtime⁰.Unit⁰ //│ else //│ set tmp = -⁰(n, 1); -//│ return annotLoop¹((tmp as! Int⁰)) +//│ return annotLoop¹(tmp) //│ end //│ }; //│ return annotHelper⁰(100000) //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let annotLoop_annotHelper; //│ @private -//│ define annotLoop_annotHelper as fun annotLoop_annotHelper⁰(id: Int⁰, param0: ?): ? { +//│ define annotLoop_annotHelper as fun annotLoop_annotHelper⁰(id: Int⁰, param0: ?): Unknown { //│ loop loopLabel: //│ match id //│ 0 => //│ set id = 1; //│ continue loopLabel //│ 1 => -//│ let scrut: ?, tmp: ?; +//│ let scrut: ?, tmp: Int⁰; //│ set scrut = <=⁰(param0, 0); //│ match scrut //│ true => //│ return runtime⁰.Unit⁰ //│ else //│ set tmp = -⁰(param0, 1); -//│ set param0 = (tmp as! Int⁰); +//│ set param0 = tmp; //│ set id = 0; //│ continue loopLabel //│ end @@ -704,7 +702,7 @@ annotHelper(100000) //│ return (annotLoop_annotHelper⁰(0, n) as! Unit⁰) //│ }; //│ @inline -//│ define annotHelper¹ as fun annotHelper⁰(n: ?): ? { +//│ define annotHelper¹ as fun annotHelper⁰(n: Int⁰): Unknown { //│ return annotLoop_annotHelper⁰(1, n) //│ }; //│ return annotLoop_annotHelper⁰(1, 100000) @@ -724,19 +722,19 @@ sccHelper(100000) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ @tailrec //│ define sccLoop⁰ as fun sccLoop¹(n: Int⁰): Int⁰ { -//│ let r: ?; +//│ let r: Num⁰; //│ set r = sccHelper⁰(n); //│ return (r as! Int⁰) //│ }; //│ define sccHelper¹ as fun sccHelper⁰(n: Int⁰): Num⁰ { -//│ let scrut: ?, tmp: ?; +//│ let scrut: ?, tmp: Int⁰; //│ set scrut = <=⁰(n, 0); //│ match scrut //│ true => //│ return 0 //│ else //│ set tmp = -⁰(n, 1); -//│ return sccLoop¹((tmp as! Int⁰)) +//│ return sccLoop¹(tmp) //│ end //│ }; //│ return sccHelper⁰(100000) @@ -750,14 +748,14 @@ sccHelper(100000) //│ set id = 1; //│ continue loopLabel //│ 1 => -//│ let scrut: ?, tmp: ?; +//│ let scrut: ?, tmp: Int⁰; //│ set scrut = <=⁰(param0, 0); //│ match scrut //│ true => //│ return 0 //│ else //│ set tmp = -⁰(param0, 1); -//│ set param0 = (tmp as! Int⁰); +//│ set param0 = tmp; //│ set id = 0; //│ continue loopLabel //│ end @@ -787,9 +785,9 @@ useFn(curried(1)) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let tmp: Function⁰; //│ define curried⁰ as fun curried¹(a: Int⁰)(b: Int⁰): Int⁰ { -//│ return (+⁰(a, b) as! Int⁰) +//│ return +⁰(a, b) //│ }; -//│ define useFn⁰ as fun useFn¹(g: Function⁰): ? { +//│ define useFn⁰ as fun useFn¹(g: Function⁰): Unknown { //│ return g(1) //│ }; //│ set tmp = curried¹(1); @@ -813,7 +811,7 @@ val over: Int = thunk(2) //│ define lambda as fun lambda⁰(b: Int⁰): ? { //│ return +⁰(b, 1) //│ }; -//│ return (lambda⁰ as! Function⁰) +//│ return lambda⁰ //│ }; //│ set tmp = thunk¹(); //│ set tmp1 = tmp(2); @@ -824,7 +822,7 @@ val over: Int = thunk(2) :ge val bad: Str = thunk //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Function' at an unrelated type 'Str' -//│ ║ l.825: val bad: Str = thunk +//│ ║ l.823: val bad: Str = thunk //│ ╙── ^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let tmp: Function⁰; set tmp = thunk¹(); define bad² as val bad³: Str⁰ = tmp; end @@ -836,10 +834,10 @@ val bad: Str = thunk fun called() = 3 val notCalled: Str = called //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Function' at an unrelated type 'Str' -//│ ║ l.837: val notCalled: Str = called +//│ ║ l.835: val notCalled: Str = called //│ ╙── ^^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define called⁰ as fun called¹(): ? { +//│ define called⁰ as fun called¹(): Int⁰ { //│ return 3 //│ }; //│ define notCalled⁰ as val notCalled¹: Str⁰ = called¹; @@ -880,10 +878,10 @@ fun callMember: Int = M.memberGetter{as Function}() :ge fun badInst() = useInt(new Bar("x")) //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Bar' at an unrelated type 'Int' -//│ ║ l.881: fun badInst() = useInt(new Bar("x")) +//│ ║ l.879: fun badInst() = useInt(new Bar("x")) //│ ╙── ^^^^^^^^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define badInst⁰ as fun badInst¹(): ? { +//│ define badInst⁰ as fun badInst¹(): Int⁰ { //│ let tmp: Bar⁰; //│ set tmp = new Bar⁰("x"); //│ return useInt¹(tmp) @@ -906,8 +904,8 @@ fun test: Function = new Object() // A written arrow type erases to `Function` in every position it can be written - parameter, result and `val` -// alike - and the parameter and result shapes erase away with it. A lambda flowing into such a slot is cast -// into a `Function` on the way. +// alike - and the parameter and result shapes erase away with it. A lambda already has the `Function` +// representation and needs no cast when flowing into such a slot. fun applyF(f: Int -> Int): Int = f(1) applyF((x: Int) => x) fun mk: Int -> Int = (x: Int) => x @@ -923,33 +921,33 @@ val g: Int -> Int = (x: Int) => x //│ define lambda2 as fun lambda¹(x: Int⁰): ? { //│ return x //│ }; -//│ return (lambda¹ as! Function⁰) +//│ return lambda¹ //│ }; //│ define lambda as fun lambda²(x: Int⁰): ? { //│ return x //│ }; -//│ do applyF¹((lambda² as! Function⁰)); +//│ do applyF¹(lambda²); //│ define lambda1 as fun lambda³(x: Int⁰): ? { //│ return x //│ }; -//│ define g² as val g³: Function⁰ = (lambda³ as! Function⁰); +//│ define g² as val g³: Function⁰ = lambda³; //│ end -// The narrowing is what makes an arrow-annotated slot *typed*: without it this would pass as `Unknown`. +// An arrow-annotated parameter rejects an unrelated integer argument during erasure. :ge fun badFn() = applyF(42) //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Function' -//│ ║ l.941: fun badFn() = applyF(42) +//│ ║ l.939: fun badFn() = applyF(42) //│ ╙── ^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define badFn⁰ as fun badFn¹(): ? { return applyF¹(42) }; end +//│ define badFn⁰ as fun badFn¹(): Int⁰ { return applyF¹(42) }; end // A `Str`-typed helper for the `declare fun` blocks below. fun useStr(s: Str) = s //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define useStr⁰ as fun useStr¹(s: Str⁰): ? { return s }; end +//│ define useStr⁰ as fun useStr¹(s: Str⁰): Str⁰ { return s }; end // A `declare`d function is never auto-invoked - `Lowering.ref` lowers the reference to a `globalThis` @@ -957,7 +955,7 @@ fun useStr(s: Str) = s declare fun addInts: (Int, Int) -> Int fun useAdd() = useInt(addInts(1, 2)) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define useAdd⁰ as fun useAdd¹(): ? { +//│ define useAdd⁰ as fun useAdd¹(): Int⁰ { //│ let tmp: Int⁰; //│ set tmp = globalThis⁰.addInts⁰(1, 2); //│ return useInt¹(tmp) @@ -969,10 +967,10 @@ fun useAdd() = useInt(addInts(1, 2)) :ge fun badAdd() = useStr(addInts(1, 2)) //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Str' -//│ ║ l.970: fun badAdd() = useStr(addInts(1, 2)) +//│ ║ l.968: fun badAdd() = useStr(addInts(1, 2)) //│ ╙── ^^^^^^^^^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define badAdd⁰ as fun badAdd¹(): ? { +//│ define badAdd⁰ as fun badAdd¹(): Str⁰ { //│ let tmp: Int⁰; //│ set tmp = globalThis⁰.addInts⁰(1, 2); //│ return useStr¹(tmp) @@ -984,7 +982,7 @@ fun badAdd() = useStr(addInts(1, 2)) declare fun showInt: Int -> Str fun useShow() = useStr(showInt(1)) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define useShow⁰ as fun useShow¹(): ? { +//│ define useShow⁰ as fun useShow¹(): Str⁰ { //│ let tmp: Str⁰; //│ set tmp = globalThis⁰.showInt⁰(1); //│ return useStr¹(tmp) @@ -998,7 +996,7 @@ fun useShow() = useStr(showInt(1)) declare fun curried: Int -> Int -> Int fun useCurried() = useInt(curried(1)(2)) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define useCurried⁰ as fun useCurried¹(): ? { +//│ define useCurried⁰ as fun useCurried¹(): Int⁰ { //│ let callPrefix: Function⁰, tmp: ?; //│ set callPrefix = globalThis⁰.curried²(1); //│ set tmp = callPrefix(2); @@ -1008,7 +1006,7 @@ fun useCurried() = useInt(curried(1)(2)) fun usePartial() = useFn(curried(1)) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define usePartial⁰ as fun usePartial¹(): ? { +//│ define usePartial⁰ as fun usePartial¹(): Unknown { //│ let tmp: Function⁰; //│ set tmp = globalThis⁰.curried²(1); //│ return useFn¹(tmp) @@ -1020,7 +1018,7 @@ fun usePartial() = useFn(curried(1)) declare fun parseIt(s: Str, radix: Int): Int fun useParse() = useInt(parseIt("1", 10)) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define useParse⁰ as fun useParse¹(): ? { +//│ define useParse⁰ as fun useParse¹(): Int⁰ { //│ let tmp: Int⁰; //│ set tmp = globalThis⁰.parseIt⁰("1", 10); //│ return useInt¹(tmp) @@ -1033,7 +1031,7 @@ fun useParse() = useInt(parseIt("1", 10)) fun getFn: Int -> Int fun useGetFn() = useFn(getFn) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define useGetFn⁰ as fun useGetFn¹(): ? { +//│ define useGetFn⁰ as fun useGetFn¹(): Unknown { //│ let tmp: Function⁰; //│ set tmp = getFn⁰(); //│ return useFn¹(tmp) @@ -1056,7 +1054,9 @@ fun viaUntyped(x, y): Str = @untyped addInts(x, y) // Without the annotation the same body is rejected - the guard rail doing its job. :ge fun viaTyped(x, y): Str = addInts(x, y) -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Str' +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Str' +//│ ║ l.1056: fun viaTyped(x, y): Str = addInts(x, y) +//│ ╙── ^^^^^^^^^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define viaTyped⁰ as fun viaTyped¹(x: ?, y: ?): Str⁰ { return globalThis⁰.addInts⁰(x, y) }; end @@ -1072,7 +1072,7 @@ module Plain with //│ let lambda; //│ @private //│ define lambda as fun lambda⁴(x: Int⁰): ? { return x }; -//│ return (lambda⁴ as! Function⁰) +//│ return lambda⁴ //│ } //│ }; //│ end @@ -1086,18 +1086,18 @@ declare module Ext with // Selecting the member yields the function value the signature describes. fun useExt() = useFn(Ext.member) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define useExt⁰ as fun useExt¹(): ? { return useFn¹(globalThis⁰.Ext⁰.member⁰) }; end +//│ define useExt⁰ as fun useExt¹(): Unknown { return useFn¹(globalThis⁰.Ext⁰.member⁰) }; end fun usePlain() = useFn(Plain.f) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define usePlain⁰ as fun usePlain¹(): ? { return useFn¹(Plain².f⁷) }; end +//│ define usePlain⁰ as fun usePlain¹(): Unknown { return useFn¹(Plain².f⁷) }; end // *Applying* the selected member is a call on that function value, and since `Function` does not store its // result types, a checked downcast is inserted at the use site. fun applyExt() = useInt(Ext.member(1)) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define applyExt⁰ as fun applyExt¹(): ? { +//│ define applyExt⁰ as fun applyExt¹(): Int⁰ { //│ let tmp: ?; //│ set tmp = globalThis⁰.Ext⁰.member⁰(1); //│ return useInt¹((tmp as! Int⁰)) @@ -1106,7 +1106,7 @@ fun applyExt() = useInt(Ext.member(1)) fun applyPlain() = useInt(Plain.f(1)) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define applyPlain⁰ as fun applyPlain¹(): ? { +//│ define applyPlain⁰ as fun applyPlain¹(): Int⁰ { //│ let tmp: ?; //│ set tmp = Plain².f⁷(1); //│ return useInt¹((tmp as! Int⁰)) @@ -1118,7 +1118,7 @@ fun applyPlain() = useInt(Plain.f(1)) // local, which carries no parameter lists to match. fun applyGetFn() = useInt(getFn(1)) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define applyGetFn⁰ as fun applyGetFn¹(): ? { +//│ define applyGetFn⁰ as fun applyGetFn¹(): Int⁰ { //│ let tmp: Function⁰, tmp1: ?; //│ set tmp = getFn⁰(); //│ set tmp1 = tmp(1); @@ -1140,7 +1140,9 @@ fun refUntyped(x: Int): Str = @untyped x :ge fun refTyped(x: Int): Str = x -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Str' +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Str' +//│ ║ l.1142: fun refTyped(x: Int): Str = x +//│ ╙── ^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define refTyped⁰ as fun refTyped¹(x: Int⁰): Str⁰ { return x }; end @@ -1158,7 +1160,9 @@ fun selUntyped(): Int = @untyped Plain.f :ge fun selTyped(): Int = Plain.f -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Function' at an unrelated type 'Int' +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Function' at an unrelated type 'Int' +//│ ║ l.1162: fun selTyped(): Int = Plain.f +//│ ╙── ^^^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define selTyped⁰ as fun selTyped¹(): Int⁰ { return Plain².f⁷ }; end @@ -1182,23 +1186,24 @@ fun lamUntyped(): Int = @untyped ((x: Int) => x) // The simplifier propagates a cast literal to its use sites as the whole `Cast` node; the price is that a // `Cast` now sits between a literal and its consumers, so a consumer that wants the literal has to look // through it. -// The two sites below are where that is a correctness matter: without the look-through they emit `42.whoami` +// The explicit `Any` result keeps the narrowing cast present before simplification. +// The two sites below are where looking through that cast matters: otherwise they emit `42.whoami` // and `42.length`, which are syntax errors - the `.` parses as a decimal point. :js class Wrapper with val whoami = "wrapper" -fun fortyTwo = 42 +fun fortyTwo: Any = 42 fun asWrapper(x: Wrapper): Wrapper = x //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define fortyTwo⁰ as fun fortyTwo¹(): ? { +//│ define fortyTwo⁰ as fun fortyTwo¹(): Unknown { //│ return 42 //│ }; //│ define asWrapper⁰ as fun asWrapper¹(x: Wrapper⁰): Wrapper⁰ { //│ return x //│ }; //│ define Wrapper¹ as class Wrapper⁰ { -//│ val whoami⁰: ?; -//│ constructor() { define whoami⁰ as val whoami¹: ? = "wrapper"; end } +//│ val whoami⁰: Str⁰; +//│ constructor() { define whoami⁰ as val whoami¹: Str⁰ = "wrapper"; end } //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -1209,7 +1214,7 @@ fun asWrapper(x: Wrapper): Wrapper = x :sjs asWrapper(fortyTwo).whoami //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ let tmp: ?, tmp1: Wrapper⁰; +//│ let tmp: Unknown, tmp1: Wrapper⁰; //│ set tmp = fortyTwo¹(); //│ set tmp1 = asWrapper¹((tmp as!! Wrapper⁰)); //│ return tmp1.whoami¹ @@ -1225,7 +1230,7 @@ if asWrapper(fortyTwo) is [a, b] then "pair" else "other" //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ let scrut: ?, a: ?, b: ?, element1$: ?, element0$: ?, tmp: ?; +//│ let scrut: ?, a: ?, b: ?, element1$: ?, element0$: ?, tmp: Unknown; //│ set tmp = fortyTwo¹(); //│ set scrut = asWrapper¹((tmp as!! Wrapper⁰)); //│ match scrut @@ -1258,7 +1263,7 @@ class Box(content: Int) with peek() //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let peek; -//│ define peek as fun peek⁰(Box: Box⁰): ? { +//│ define peek as fun peek⁰(Box: Box⁰): Box⁰ { //│ return Box //│ }; //│ define Box¹ as class Box⁰ { @@ -1268,7 +1273,7 @@ class Box(content: Int) with //│ define content¹ as val content⁰: Int⁰ = content; //│ end //│ } -//│ method unwrap⁰ = fun unwrap¹(): ? { return peek⁰(Box⁰.this) } +//│ method unwrap⁰ = fun unwrap¹(): Box⁰ { return peek⁰(Box⁰.this) } //│ }; //│ end @@ -1283,17 +1288,17 @@ fun tagged(x: Int) = fun describe() = Tag.label() describe() //│ ╔══[WARNING] Objects are not yet lifted. -//│ ║ l.1281: object Tag with +//│ ║ l.1286: object Tag with //│ ╙── ^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let describe; -//│ define describe as fun describe⁰(Tag: Tag⁰): ? { +//│ define describe as fun describe⁰(Tag: Tag⁰): Str⁰ { //│ return Tag.label⁰() //│ }; -//│ define tagged⁰ as fun tagged¹(x: Int⁰): ? { +//│ define tagged⁰ as fun tagged¹(x: Int⁰): Str⁰ { //│ let Tag; //│ define Tag as object Tag⁰ { -//│ method label¹ = fun label⁰(): ? { return "tag" } +//│ method label¹ = fun label⁰(): Str⁰ { return "tag" } //│ }; //│ return describe⁰(Tag⁰) //│ }; @@ -1310,7 +1315,7 @@ fun mk() = fun get() = Tag.label() new Holder //│ ╔══[WARNING] Objects are not yet lifted. -//│ ║ l.1307: object Tag with +//│ ║ l.1312: object Tag with //│ ╙── ^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let Holder; @@ -1320,14 +1325,14 @@ fun mk() = //│ set Holder⁰.this.Tag¹ = Tag; //│ end //│ } -//│ method get³ = fun get⁴(): ? { +//│ method get³ = fun get⁴(): Str⁰ { //│ return Holder⁰.this.Tag¹.label²() //│ } //│ }; -//│ define mk⁴ as fun mk⁵(): ? { +//│ define mk⁴ as fun mk⁵(): Holder⁰ { //│ let Tag; //│ define Tag as object Tag² { -//│ method label³ = fun label²(): ? { return "tag" } +//│ method label³ = fun label²(): Str⁰ { return "tag" } //│ }; //│ return new Holder⁰(Tag²) //│ }; @@ -1346,8 +1351,8 @@ fun capturingLoop(n: Int, acc: Str) = let h = g capturingLoop(n - 1, h()) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define capturingLoop⁰ as fun capturingLoop¹(n: Int⁰, acc: Str⁰): ? { -//│ let scrut: ?, g: ?, h: ?, tmp: ?, tmp1: ?; +//│ define capturingLoop⁰ as fun capturingLoop¹(n: Int⁰, acc: Str⁰): Str⁰ { +//│ let scrut: ?, g: Function⁰, h: Function⁰, tmp: Int⁰, tmp1: ?; //│ set scrut = <=⁰(n, 0); //│ match scrut //│ true => @@ -1362,14 +1367,14 @@ fun capturingLoop(n: Int, acc: Str) = //│ set h = g; //│ set tmp = -⁰(n, 1); //│ set tmp1 = h(); -//│ return capturingLoop¹((tmp as! Int⁰), (tmp1 as! Str⁰)) +//│ return capturingLoop¹(tmp, (tmp1 as! Str⁰)) //│ end //│ }; //│ end //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ define capturingLoop⁰ as fun capturingLoop¹(n: Int⁰, acc: Str⁰): ? { +//│ define capturingLoop⁰ as fun capturingLoop¹(n: Int⁰, acc: Str⁰): Str⁰ { //│ loop loopLabel: -//│ let scrut: ?, tmp: ?, tmp1: ?, acc1: Str⁰; +//│ let scrut: ?, tmp: Int⁰, tmp1: ?, acc1: Str⁰; //│ set acc1 = acc; //│ set scrut = <=⁰(n, 0); //│ match scrut @@ -1383,7 +1388,7 @@ fun capturingLoop(n: Int, acc: Str) = //│ }; //│ set tmp = -⁰(n, 1); //│ set tmp1 = g⁴(); -//│ set n = (tmp as! Int⁰); +//│ set n = tmp; //│ set acc = (tmp1 as! Str⁰); //│ continue loopLabel //│ end diff --git a/hkmc2/shared/src/test/mlscript/codegen/ImportExample.mls b/hkmc2/shared/src/test/mlscript/codegen/ImportExample.mls index 16662823a3..03eb9cc6a4 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ImportExample.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ImportExample.mls @@ -58,7 +58,11 @@ inc / 2 :sjs :re +:ge n / 2 +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Function' +//│ ║ l.62: n / 2 +//│ ╙── ^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ return runtime.safeCall(n(2)) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/codegen/ImportMLs.mls b/hkmc2/shared/src/test/mlscript/codegen/ImportMLs.mls index cfa8f62aa8..a5d8e3abd6 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ImportMLs.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ImportMLs.mls @@ -21,11 +21,11 @@ open Option :sjs None isDefined() //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let x2; -//│ x2 = Option.None; -//│ if (x2 instanceof Option.Some.class) { +//│ let x1; +//│ x1 = Option.None; +//│ if (x1 instanceof Option.Some.class) { //│ return true -//│ } else if (x2 instanceof Option.None.class) { +//│ } else if (x1 instanceof Option.None.class) { //│ return false //│ } //│ throw globalThis.Object.freeze(new globalThis.Error("match error")); diff --git a/hkmc2/shared/src/test/mlscript/codegen/MergeMatchArms.mls b/hkmc2/shared/src/test/mlscript/codegen/MergeMatchArms.mls index ca86b55a2a..9d5aa2573b 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/MergeMatchArms.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/MergeMatchArms.mls @@ -119,7 +119,7 @@ print(x) //│ throw globalThis.Object.freeze(new globalThis.Error("match error")) //│ } //│ x = tmp; -//│ return Predef.print(tmp) +//│ return Predef.print(x) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ > 1 //│ x = 1 diff --git a/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls b/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls index 203e27bf1c..578431bcbb 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls @@ -108,27 +108,29 @@ i(print)(2) //│ ╙── ^^^^^^^^^^ //│ ═══[RUNTIME ERROR] Error: Function 'i' expected at least 2 arguments but got 1 -// :re +// The sum flows to every call of `i`'s argument, including the ill-arity `i(print)(2)` above, +// whose application of that result is reported when this block resolves the sum. +:e i((x, y, z) => x + y + z, 2) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.102: i(print)(2) +//│ ║ ^^^^^^^^^^^ +//│ ╟── Value of type 'Int' cannot be called like a function. +//│ ║ l.114: i((x, y, z) => x + y + z, 2) +//│ ╙── ^^^^^^^^^ //│ = 3 // :re :e i((...args) => [...args], _)(2) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.102: i(print)(2) -//│ ║ ^^^^^^^^^^^ -//│ ╟── Tuple literal cannot be called like a function. -//│ ║ l.117: i((...args) => [...args], _)(2) -//│ ║ ^^^^ -//│ ╟── Tuple literal cannot be called like a function. -//│ ║ l.91: let i = _(0, 1, _) -//│ ╙── ^^^^^^^^^ -//│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.117: i((...args) => [...args], _)(2) +//│ ║ l.125: i((...args) => [...args], _)(2) //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Value of type 'Int' cannot be called like a function. +//│ ║ l.114: i((x, y, z) => x + y + z, 2) +//│ ║ ^^^^^^^^^ //│ ╟── Tuple literal cannot be called like a function. -//│ ║ l.117: i((...args) => [...args], _)(2) +//│ ║ l.125: i((...args) => [...args], _)(2) //│ ║ ^^^^ //│ ╟── Tuple literal cannot be called like a function. //│ ║ l.91: let i = _(0, 1, _) @@ -150,15 +152,18 @@ i((...args) => [...args], _)(2) :re 123 |> (_ + 1 >> _ * 2) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.151: 123 |> (_ + 1 >> _ * 2) +//│ ║ l.153: 123 |> (_ + 1 >> _ * 2) //│ ║ ^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Callback parameter list does not match its declared function type. -//│ ║ l.151: 123 |> (_ + 1 >> _ * 2) +//│ ║ l.153: 123 |> (_ + 1 >> _ * 2) //│ ║ ^^^^^^^^^^^^^^ //│ ╟── The expected function type is declared here. //│ ║ in ../../mlscript-compile/Predef.mls //│ ║ l.30: fun (|>) pipeInto[A, B](x: A, f: A -> B): B = f(x) //│ ╙── ^^^^^^ +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Function' +//│ ║ l.153: 123 |> (_ + 1 >> _ * 2) +//│ ╙── ^^^^^ //│ ═══[RUNTIME ERROR] Error: Function expected at least 2 arguments but got 1 (_ + 1 + _ * 2) of 123, 456 @@ -199,7 +204,12 @@ let j = _.x.y(1, _) in j((x: y: tuple), 0) data class C(a, b, c) +// Without parentheses the placeholder abstracts over the whole pipeline, which calls a C instance. +:ge 2 |> C(1, _, 3) |> print +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'C' at an unrelated type 'Function' +//│ ║ l.209: 2 |> C(1, _, 3) |> print +//│ ╙── ^^^^^^^^^^ //│ = fun 2 |> (C(1, _, 3)) |> print @@ -248,7 +258,7 @@ _ - _ of 1, 2 :pe |> 1 //│ ╔══[PARSE ERROR] Expected end of input; found literal instead -//│ ║ l.249: |> 1 +//│ ║ l.259: |> 1 //│ ╙── ^ //│ = fun pipeInto @@ -256,7 +266,7 @@ _ - _ of 1, 2 :re |> (1) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.257: |> (1) +//│ ║ l.267: |> (1) //│ ║ ^^^^^^ //│ ╟── Function 'pipeInto' expected 2 arguments, but got 1 //│ ║ in ../../mlscript-compile/Predef.mls @@ -278,21 +288,21 @@ _ - _ of 1, 2 :sjs 1 |. (_ - 2) //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let lambda32; -//│ lambda32 = (undefined, function (_0) { return _0 - 2 }); -//│ return Predef.passTo(1, lambda32) +//│ let lambda8; +//│ lambda8 = (undefined, function (_0) { return _0 - 2 }); +//│ return Predef.passTo(1, lambda8) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = fun :sjs 1 |. (_ - 2)() //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let lambda33, args; -//│ lambda33 = (undefined, function (_0) { +//│ let lambda9, args; +//│ lambda9 = (undefined, function (_0) { //│ return _0 - 2 //│ }); //│ args = []; -//│ return runtime.safeCall(lambda33(1, ...args)) +//│ return runtime.safeCall(lambda9(1, ...args)) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = -1 @@ -305,13 +315,13 @@ _ - _ of 1, 2 :w let f = if _ then 1 else 0 //│ ╔══[WARNING] This catch-all clause makes the following branches unreachable. -//│ ║ l.306: let f = if _ then 1 else 0 +//│ ║ l.316: let f = if _ then 1 else 0 //│ ║ ^^^^^^ //│ ╟── This branch is unreachable. -//│ ║ l.306: let f = if _ then 1 else 0 +//│ ║ l.316: let f = if _ then 1 else 0 //│ ╙── ^^^^^^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let f23; f23 = 1; +//│ let f13; f13 = 1; //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ f = 1 @@ -321,8 +331,8 @@ fun f(x) = 0 then 1 _ then 2 //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let f24; -//│ f24 = function f(x3) { +//│ let f14; +//│ f14 = function f(x3) { //│ let scrut; //│ scrut = Predef.equals(x3, 0); //│ if (scrut === true) { return 1 } diff --git a/hkmc2/shared/src/test/mlscript/codegen/PrivateMemberExtrusion.mls b/hkmc2/shared/src/test/mlscript/codegen/PrivateMemberExtrusion.mls index 2b436ae7b1..59cdbe55ca 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/PrivateMemberExtrusion.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/PrivateMemberExtrusion.mls @@ -144,7 +144,7 @@ module Pair with //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ const sum$accessorSymbol = globalThis.Symbol("sum"); //│ const scaled$accessorSymbol = globalThis.Symbol("scaled"); -//│ let Pair1, tmp3, tmp4, p, inlinedVal, p1, inlinedVal1; +//│ let Pair1, tmp3, tmp4, inlinedVal, inlinedVal1; //│ Pair1 = function Pair(fst, snd) { //│ return globalThis.Object.freeze(new Pair.class(fst, snd)); //│ }; @@ -162,11 +162,11 @@ module Pair with //│ static #scale(n, k) { //│ return n * k //│ } -//│ static sumOf(p2) { -//│ return p2.#sum +//│ static sumOf(p) { +//│ return p.#sum //│ } -//│ static scaledOf(p2, k) { -//│ return p2.#scaled(k) +//│ static scaledOf(p, k) { +//│ return p.#scaled(k) //│ } //│ #sum; //│ get [sum$accessorSymbol]() { return this.#sum; } @@ -181,11 +181,9 @@ module Pair with //│ static [definitionMetadata] = ["class", "Pair", ["fst", "snd"]]; //│ }); //│ tmp3 = Pair1(1, 2); -//│ p = tmp3; -//│ inlinedVal = p[sum$accessorSymbol]; +//│ inlinedVal = tmp3[sum$accessorSymbol]; //│ tmp4 = Pair1(1, 2); -//│ p1 = tmp4; -//│ inlinedVal1 = p1[scaled$accessorSymbol](10); +//│ inlinedVal1 = tmp4[scaled$accessorSymbol](10); //│ return globalThis.Object.freeze([ inlinedVal, inlinedVal1 ]) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = [3, 30] @@ -260,13 +258,13 @@ class PrivateOverride extends PublicBase with let po = new PrivateOverride [po.viaThis, po.pick(true), po.pick(false)] //│ ╔══[COMPILATION ERROR] This reference may target a private member, which cannot be selected together with other members -//│ ║ l.259: fun pick(flag) = (if flag then this else new PublicBase).f +//│ ║ l.257: fun pick(flag) = (if flag then this else new PublicBase).f //│ ║ ^^ //│ ╟── private member 'f' declared here -//│ ║ l.257: private fun f = "B" +//│ ║ l.255: private fun f = "B" //│ ║ ^ //│ ╟── other target: function 'f' -//│ ║ l.255: fun f = "A" +//│ ║ l.253: fun f = "A" //│ ╙── ^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ po = PrivateOverride diff --git a/hkmc2/shared/src/test/mlscript/codegen/ScopedBlocks.mls b/hkmc2/shared/src/test/mlscript/codegen/ScopedBlocks.mls index 62b3378edc..5231ceac3b 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ScopedBlocks.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ScopedBlocks.mls @@ -75,7 +75,8 @@ fun f() = if A(1, Nil) is -:e +// Every branch repeats, so this loop never assigns Unit to x. +:sjs fun f(x) = x = while x is A(a, _) then a() @@ -83,10 +84,22 @@ fun f(x) = else x() x -//│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.84: x() -//│ ║ ^^^ -//│ ╙── Unit value cannot be called like a function. +//│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— +//│ let f3; +//│ f3 = function f(x1) { +//│ lbl: while (true) { +//│ if (x1 instanceof A1.class) { +//│ runtime.safeCall(x1.a()); +//│ continue lbl +//│ } else if (x1 instanceof B1.class) { +//│ runtime.safeCall(x1.a()); +//│ continue lbl +//│ } +//│ runtime.safeCall(x1()); +//│ continue lbl; +//│ } +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/codegen/SetIn.mls b/hkmc2/shared/src/test/mlscript/codegen/SetIn.mls index 062fe5bb07..21e050953c 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/SetIn.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/SetIn.mls @@ -123,7 +123,7 @@ fun example() = example() //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define example⁰ as fun example¹() { -//│ let x, get_x, old, tmp, tmp1, tmp2, tmp3, get_x1; +//│ let x, get_x, y, old, tmp, tmp1, tmp2, tmp3, get_x1; //│ set x = 0; //│ @private //│ define get_x1 as fun get_x⁰() { @@ -141,10 +141,11 @@ example() //│ finally //│ set x = old; //│ end; -//│ do Predef⁰.print⁰(x); +//│ set y = (tmp as! Int⁰); +//│ do Predef⁰.print⁰(x); //│ set tmp3 = get_x(); //│ do Predef⁰.print⁰(tmp3); -//│ return tmp +//│ return y //│ }; //│ return example¹() //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/dead-param-elim/clash.mls b/hkmc2/shared/src/test/mlscript/dead-param-elim/clash.mls index 77cf55bfaf..d1190147a6 100644 --- a/hkmc2/shared/src/test/mlscript/dead-param-elim/clash.mls +++ b/hkmc2/shared/src/test/mlscript/dead-param-elim/clash.mls @@ -97,7 +97,7 @@ m() //│ let tmp, lambda; //│ set tmp = [1, 2, 3]; //│ define lambda as fun lambda⁰(x, i, a) { return +⁰(x, 1) }; -//│ return tmp.map⁰((lambda⁰ as! Function⁰)) +//│ return tmp.map⁰(lambda⁰) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = [2, 3, 4] diff --git a/hkmc2/shared/src/test/mlscript/dead-param-elim/todos.mls b/hkmc2/shared/src/test/mlscript/dead-param-elim/todos.mls index 765511bcc2..c3ff9ff982 100644 --- a/hkmc2/shared/src/test/mlscript/dead-param-elim/todos.mls +++ b/hkmc2/shared/src/test/mlscript/dead-param-elim/todos.mls @@ -44,7 +44,7 @@ c(p(true)) //│ dead-param-elim > >>> dead-param-elim results >>> //│ dead-param-elim > <<< dead-param-elim results <<< //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let p$p$worker; +//│ let p$p$worker, inlinedVal; //│ @private @noInline //│ define p$p$worker as fun p$p$worker⁰(b, eta$0$0) { //│ match b @@ -73,7 +73,8 @@ c(p(true)) //│ return 0 //│ end //│ }; -//│ return p$p$worker⁰(true, 0) +//│ set inlinedVal = p$p$worker⁰(true, 0); +//│ return (inlinedVal as! Int⁰) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 1 @@ -104,7 +105,7 @@ sumPlus(enum(3)) //│ dead-param-elim > >>> dead-param-elim results >>> //│ dead-param-elim > <<< dead-param-elim results <<< //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let enum$enum$worker; +//│ let enum$enum$worker, inlinedVal; //│ @private //│ define enum$enum$worker as fun enum$enum$worker⁰(x, eta$0$0, eta$0$1, eta$0$2, eta$0$3, eta$0$4, eta$0$5, eta$0$6) { //│ let scrut, tmp; @@ -113,10 +114,11 @@ sumPlus(enum(3)) //│ true => //│ return 1 //│ else -//│ let fv_tmp, inlinedVal; +//│ let fv_tmp, inlinedVal1, inlinedVal2; //│ set tmp = -⁰(x, 1); -//│ set inlinedVal = enum$enum$worker⁰(tmp, undefined, undefined, undefined, undefined, undefined, undefined, undefined); -//│ set fv_tmp = +⁰(x, inlinedVal); +//│ set inlinedVal2 = enum$enum$worker⁰(tmp, undefined, undefined, undefined, undefined, undefined, undefined, undefined); +//│ set inlinedVal1 = (inlinedVal2 as! Int⁰); +//│ set fv_tmp = +⁰(x, inlinedVal1); //│ return +⁰(fv_tmp, 1) //│ end //│ }; @@ -147,7 +149,8 @@ sumPlus(enum(3)) //│ throw new globalThis⁰.Error⁰("match error") //│ end //│ }; -//│ return enum$enum$worker⁰(3, undefined, undefined, undefined, undefined, undefined, undefined, undefined) +//│ set inlinedVal = enum$enum$worker⁰(3, undefined, undefined, undefined, undefined, undefined, undefined, undefined); +//│ return (inlinedVal as! Int⁰) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 11 @@ -182,7 +185,7 @@ c(p(true)) //│ dead-param-elim > >>> dead-param-elim results >>> //│ dead-param-elim > <<< dead-param-elim results <<< //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let p$p$worker; +//│ let p$p$worker, inlinedVal; //│ @private @noInline //│ define p$p$worker as fun p$p$worker¹(b) { //│ match b @@ -211,6 +214,7 @@ c(p(true)) //│ throw new globalThis⁰.Error⁰("match error") //│ end //│ }; -//│ return p$p$worker¹(true) +//│ set inlinedVal = p$p$worker¹(true); +//│ return (inlinedVal as! Int⁰) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 1 diff --git a/hkmc2/shared/src/test/mlscript/deforest/basic.mls b/hkmc2/shared/src/test/mlscript/deforest/basic.mls index 75dcc7c366..8c88d03d8b 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/basic.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/basic.mls @@ -23,7 +23,7 @@ let lsit = [2, 1] in //│ deforest > fields: runtime⁰.Tuple⁰.get⁰(lsit⁰, 1) //│ deforest > <<< fusing <<< //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ let lsit, useless, a, b, element1$, element0$, tmp, $lsit_tup2, $lsit_rest, tup2_0, tup2_1; +//│ let useless, a, b, element1$, element0$, tmp, $lsit_tup2, $lsit_rest, lsit, tup2_0, tup2_1; //│ @affine(1) @private //│ define $lsit_tup2 as fun $lsit_tup2⁰(tup2_01, tup2_11)() { //│ set element0$ = tup2_01; @@ -72,7 +72,7 @@ f(p()) //│ deforest > fields: arg$X$0$⁰.b⁰ //│ deforest > <<< fusing <<< //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ let tmp, p$p, f$f, f$x_X, f$x_rest, f$arg$X$0$_Y, f$arg$X$0$_rest; +//│ let f$x_X, f$x_rest, f$arg$X$0$_Y, f$arg$X$0$_rest, p$p, f$f, tmp; //│ @private //│ define p$p as fun p$p⁰() { //│ let tmp1, Y_b, X_a; @@ -83,8 +83,8 @@ f(p()) //│ }; //│ @private //│ define f$f as fun f$f⁰(x) { -//│ let y, arg$X$0$, arg$Y$0$; -//│ return x(arg$X$0$, arg$Y$0$, y) +//│ let y, arg$Y$0$, arg$X$0$; +//│ return (x(arg$X$0$, arg$Y$0$, y) as! O⁰) //│ }; //│ @affine(1) @private //│ define f$x_X as fun f$x_X⁰(X_a)(fv_arg$X$0$, fv_arg$Y$0$, fv_y) { @@ -216,7 +216,7 @@ if [1, 2] is //│ deforest > fields: runtime⁰.Tuple⁰.get⁰(scrut⁰, 1) //│ deforest > <<< fusing <<< //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ let scrut, a, b, element1$, element0$, $scrut_tup2, $scrut_rest, tup2_0, tup2_1; +//│ let a, b, element1$, element0$, $scrut_tup2, $scrut_rest, scrut, tup2_0, tup2_1; //│ @affine(1) @private //│ define $scrut_tup2 as fun $scrut_tup2⁰(tup2_01, tup2_11)() { //│ set element0$ = tup2_01; diff --git a/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls b/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls index 79004e9403..d1ab1dabfb 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/cyclic.mls @@ -142,7 +142,7 @@ modSome(Some(17), 3) //│ break inlinedLbl //│ end //│ unreachable /* Rest of abortive labelled block */ -//│ return inlinedVal +//│ return (inlinedVal as! Object⁰) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = Some(2) diff --git a/hkmc2/shared/src/test/mlscript/deforest/determinism.mls b/hkmc2/shared/src/test/mlscript/deforest/determinism.mls index b99985149d..716d24776b 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/determinism.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/determinism.mls @@ -19,7 +19,7 @@ if B(1,2,3,4,5) is //│ deforest > fields: scrut⁰.e⁰ //│ deforest > <<< fusing <<< //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ let scrut, e, a, b, c, d, arg$B$0$, arg$B$1$, arg$B$2$, arg$B$3$, arg$B$4$, $scrut_B, $scrut_rest, B_a, B_b, B_c, B_d, B_e; +//│ let e, a, b, c, d, arg$B$0$, arg$B$1$, arg$B$2$, arg$B$3$, arg$B$4$, $scrut_B, $scrut_rest, scrut, B_a, B_b, B_c, B_d, B_e; //│ @affine(1) @private //│ define $scrut_B as fun $scrut_B⁰(B_a1, B_b1, B_c1, B_d1, B_e1)() { //│ set arg$B$0$ = B_a1; @@ -75,11 +75,11 @@ c2 of A(A(A(A(A(A(1)))))) //│ deforest > fields: arg$A$0$⁴.a¹ //│ deforest > <<< fusing <<< //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ let tmp, tmp1, tmp2, tmp3, tmp4, tmp5, c2$c2, c2$x_A, c2$x_rest, c2$arg$A$0$_A, c2$arg$A$0$_rest, c2$arg$A$0$_A1, c2$arg$A$0$_rest1, c2$arg$A$0$_A2, c2$arg$A$0$_rest2, c2$arg$A$0$_A3, c2$arg$A$0$_rest3, c2$arg$A$0$_A4, c2$arg$A$0$_rest4, A_a, A_a1, A_a2, A_a3, A_a4, A_a5; +//│ let c2$x_A, c2$x_rest, c2$arg$A$0$_A, c2$arg$A$0$_rest, c2$arg$A$0$_A1, c2$arg$A$0$_rest1, c2$arg$A$0$_A2, c2$arg$A$0$_rest2, c2$arg$A$0$_A3, c2$arg$A$0$_rest3, c2$arg$A$0$_A4, c2$arg$A$0$_rest4, c2$c2, tmp, tmp1, tmp2, tmp3, tmp4, tmp5, A_a, A_a1, A_a2, A_a3, A_a4, A_a5; //│ @private //│ define c2$c2 as fun c2$c2⁰(x) { //│ let a, arg$A$0$, arg$A$0$1, arg$A$0$2, arg$A$0$3, arg$A$0$4, arg$A$0$5; -//│ return x(arg$A$0$, arg$A$0$1, arg$A$0$2, arg$A$0$3, arg$A$0$4, arg$A$0$5, a) +//│ return (x(arg$A$0$1, arg$A$0$2, arg$A$0$3, arg$A$0$4, arg$A$0$5, arg$A$0$, a) as! Int⁰) //│ }; //│ @affine(1) @private //│ define c2$x_A as fun c2$x_A⁰(A_a6)(fv_arg$A$0$, fv_arg$A$0$1, fv_arg$A$0$2, fv_arg$A$0$3, fv_arg$A$0$4, fv_arg$A$0$5, fv_a) { diff --git a/hkmc2/shared/src/test/mlscript/deforest/erasedTypes.mls b/hkmc2/shared/src/test/mlscript/deforest/erasedTypes.mls index f326940fae..86df60f8ce 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/erasedTypes.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/erasedTypes.mls @@ -1,5 +1,4 @@ :. -:global :sir :siret :deforest @@ -57,7 +56,7 @@ fun selected(n: Int, k: Str) = //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let selected$b_Box, selected$b_rest; //│ @affine(1) @private -//│ define selected$b_Box as fun selected$b_Box⁰(Box_v: Int⁰)(fv_arg$Box$0$: ?, fv_x: ?, fv_k: Str⁰): ? { +//│ define selected$b_Box as fun selected$b_Box⁰(Box_v: Int⁰)(fv_arg$Box$0$: ?, fv_x: Int⁰, fv_k: Str⁰): ? { //│ set fv_arg$Box$0$ = Box_v; //│ set fv_x = fv_arg$Box$0$; //│ return [fv_x, fv_k] @@ -66,11 +65,11 @@ fun selected(n: Int, k: Str) = //│ define selected$b_rest as fun selected$b_rest⁰(): ? { //│ return null //│ }; -//│ define selected⁰ as fun selected¹(n: Int⁰, k: Str⁰): ? { -//│ let b: ?, x: ?, arg$Box$0$: ?, Box_v: Int⁰; +//│ define selected⁰ as fun selected¹(n: Int⁰, k: Str⁰): Array⁰ { +//│ let x: Int⁰, arg$Box$0$: ?, b: Function⁰, Box_v: Int⁰; //│ set Box_v = n; //│ set b = selected$b_Box⁰(Box_v); -//│ return b(arg$Box$0$, x, k) +//│ return (b(arg$Box$0$, x, k) as! Array⁰) //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -92,22 +91,22 @@ fun withRest(n: Int, k: Str) = //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let withRest$b_Box, withRest$b_rest; //│ @affine(1) @private -//│ define withRest$b_Box as fun withRest$b_Box⁰(Box_v: Int⁰)(fv_arg$Box$0$: ?, fv_x: ?, fv_tmp: ?, fv_r: ?, fv_k: Str⁰): ? { +//│ define withRest$b_Box as fun withRest$b_Box⁰(Box_v: Int⁰)(fv_arg$Box$0$: ?, fv_x: Int⁰, fv_tmp: Int⁰, fv_r: Int⁰, fv_k: Str⁰): ? { //│ set fv_arg$Box$0$ = Box_v; //│ set fv_x = fv_arg$Box$0$; //│ set fv_tmp = fv_x; //│ return withRest$b_rest⁰(fv_r, fv_tmp, fv_k) //│ }; //│ @private -//│ define withRest$b_rest as fun withRest$b_rest⁰(fv_r: ?, fv_tmp: ?, fv_k: Str⁰): ? { +//│ define withRest$b_rest as fun withRest$b_rest⁰(fv_r: Int⁰, fv_tmp: Int⁰, fv_k: Str⁰): ? { //│ set fv_r = fv_tmp; //│ return [fv_r, fv_k] //│ }; -//│ define withRest⁰ as fun withRest¹(n: Int⁰, k: Str⁰): ? { -//│ let b: ?, r: ?, x: ?, arg$Box$0$: ?, tmp: ?, Box_v: Int⁰; +//│ define withRest⁰ as fun withRest¹(n: Int⁰, k: Str⁰): Array⁰ { +//│ let r: Int⁰, x: Int⁰, arg$Box$0$: ?, tmp: Int⁰, b: Function⁰, Box_v: Int⁰; //│ set Box_v = n; //│ set b = withRest$b_Box⁰(Box_v); -//│ return b(arg$Box$0$, x, tmp, r, k) +//│ return (b(arg$Box$0$, x, tmp, r, k) as! Array⁰) //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -133,12 +132,914 @@ fun noFields(n: Int, s: Str) = //│ define noFields$p_rest as fun noFields$p_rest⁰(): ? { //│ return null //│ }; -//│ define noFields⁰ as fun noFields¹(n: Int⁰, s: Str⁰): ? { -//│ let p: ?, Pair_a: Int⁰, Pair_b: Str⁰; +//│ define noFields⁰ as fun noFields¹(n: Int⁰, s: Str⁰): Int⁰ { +//│ let p: Function⁰, Pair_a: Int⁰, Pair_b: Str⁰; //│ set Pair_a = n; //│ set Pair_b = s; //│ set p = noFields$p_Pair⁰(Pair_a, Pair_b); -//│ return p() +//│ return (p() as! Int⁰) //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +// Fusion changes the physical parameter and result slots even without making a polyvariant copy. +:deforest mono +:expect 42 +private fun produce(n: Int): Box = if n == 0 then Box(42) else produce(n - 1) +private fun consume(box: Box): Int = if box is Box(value) then value +consume(produce(2)) +//│ deforest > >>> fusing >>> +//│ deforest > Box¹(42) -> +//│ deforest > match: box⁰ +//│ deforest > fields: box⁰.v¹ +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let $box_Box, $box_rest, tmp: Function⁰; +//│ @affine(1) @private +//│ define $box_Box as fun $box_Box⁰(Box_v: Int⁰)(fv_arg$Box$0$: ?, fv_value: Int⁰): ? { +//│ set fv_arg$Box$0$ = Box_v; +//│ set fv_value = fv_arg$Box$0$; +//│ return fv_value +//│ }; +//│ @private +//│ define $box_rest as fun $box_rest⁰(): ? { +//│ return null +//│ }; +//│ @private +//│ define produce⁰ as fun produce¹(n: Int⁰): Function⁰ { +//│ let scrut: ?, tmp1: Int⁰; +//│ set scrut = Predef⁰.equals⁰(n, 0); +//│ match scrut +//│ true => +//│ let Box_v: Int⁰; +//│ set Box_v = 42; +//│ return $box_Box⁰(Box_v) +//│ else +//│ set tmp1 = -⁰(n, 1); +//│ return produce¹(tmp1) +//│ end +//│ }; +//│ @private +//│ define consume⁰ as fun consume¹(box: Function⁰): Int⁰ { +//│ let value: Int⁰, arg$Box$0$: ?; +//│ return (box(arg$Box$0$, value) as! Int⁰) +//│ }; +//│ set tmp = produce¹(2); +//│ return consume¹(tmp) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 42 + + +// Projecting a fused field replaces a nominal slot by the field's representation. +:deforest mono +:expect 42 +private fun pass(box: Box): Box = box +pass(Box(42)).v +//│ deforest > >>> fusing >>> +//│ deforest > Box¹(42) -> +//│ deforest > select: v +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp: Int⁰, tmp1: Int⁰, Box_v: Int⁰; +//│ @private +//│ define pass⁰ as fun pass¹(box: Int⁰): Int⁰ { +//│ return box +//│ }; +//│ set Box_v = 42; +//│ set tmp = Box_v; +//│ set tmp1 = pass¹(tmp); +//│ return tmp1 +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 42 + + + +// The cast from a physical Any result is proved by all incoming constructors. +// Field fusion changes the result, argument, and intermediate slots to Int together. +:deforest mono +:expect 42 +private fun makeOpaqueBox(): Any = Box(42) +private fun projectOpaqueBox(box: Box): Int = box.v +projectOpaqueBox(makeOpaqueBox()) +//│ deforest > >>> fusing >>> +//│ deforest > Box¹(42) -> +//│ deforest > select: v +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp: Int⁰; +//│ @private +//│ define makeOpaqueBox⁰ as fun makeOpaqueBox¹(): Int⁰ { +//│ let Box_v: Int⁰; +//│ set Box_v = 42; +//│ return Box_v +//│ }; +//│ @private +//│ define projectOpaqueBox⁰ as fun projectOpaqueBox¹(box: Int⁰): Int⁰ { +//│ return box +//│ }; +//│ set tmp = makeOpaqueBox¹(); +//│ return projectOpaqueBox¹(tmp) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 42 + + +// One unknown input prevents fusion of every producer of the same cast. +:deforest mono +:expect 43 +private fun mixedBox(x: Any): Int = + val b: Box = x + b.v +mixedBox(Box(1)) + mixedBox(hide(Box(42))) +//│ deforest > >>> fusing >>> +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp: Box², tmp1: Int⁰, tmp2: Box², tmp3: Unknown, tmp4: Int⁰; +//│ @private +//│ define mixedBox⁰ as fun mixedBox¹(x: Unknown): Int⁰ { +//│ let b: Box²; +//│ set b = (x as! Box²); +//│ return b.v¹ +//│ }; +//│ set tmp = Box¹(1); +//│ set tmp1 = mixedBox¹(tmp); +//│ set tmp2 = Box¹(42); +//│ set tmp3 = Predef⁰.hide⁰(tmp2); +//│ set tmp4 = mixedBox¹(tmp3); +//│ return +⁰(tmp1, tmp4) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 43 + + +// A fused nullary constructor becomes a closure, which its temporary and the parameter it reaches both hold. +// The call replacing the fused match returns an unknown value, which is cast into the `Int` result like the +// result of any call to a function value +:deforest mono +:expect 1 +object O +private fun isO(x: O): Int = if x is O then 1 else 0 +isO(O) +//│ deforest > >>> fusing >>> +//│ deforest > O⁰ -> +//│ deforest > match: x⁰ +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let $x_O, $x_rest, deforest$lam: Function⁰; +//│ @affine(1) @private +//│ define $x_O as fun $x_O⁰()(): ? { +//│ return 1 +//│ }; +//│ @private +//│ define $x_rest as fun $x_rest⁰(): ? { +//│ return null +//│ }; +//│ @private +//│ define isO⁰ as fun isO¹(x: Function⁰): Int⁰ { +//│ return (x() as! Int⁰) +//│ }; +//│ define O¹ as object O⁰; +//│ set deforest$lam = $x_O⁰(); +//│ return isO¹(deforest$lam) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 1 + + + +// A field of an unfused constructor cannot be retyped per instance, so `Box(42)` stays an object: its closure +// does not fit `b: Box` +:deforest mono +:expect 42 +data class H(b: Box, n: Int) +private fun get(h: H, c: Bool): Int = if c then (if h.b is Box(v) then v) else h.n +get(H(Box(42), 1), true) +//│ deforest > >>> fusing >>> +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp: Box², tmp1: H⁰; +//│ @private +//│ define get⁰ as fun get¹(h: H⁰, c: Bool⁰): Int⁰ { +//│ let scrut: ?, v: Int⁰, arg$Box$0$: ?; +//│ match c +//│ true => +//│ set scrut = h.b⁴; +//│ match scrut +//│ Box² => +//│ set arg$Box$0$ = scrut.v¹; +//│ set v = arg$Box$0$; +//│ return v +//│ else +//│ throw new globalThis⁰.Error⁰("match error") +//│ end +//│ else +//│ return h.n³ +//│ end +//│ }; +//│ @data +//│ define H¹ as class H⁰ { +//│ val b⁵: Box²; +//│ val n⁴: Int⁰; +//│ constructor H²(b: Box², n: Int⁰) { +//│ define b⁵ as val b⁴: Box² = b; +//│ define n⁴ as val n³: Int⁰ = n; +//│ end +//│ } +//│ }; +//│ set tmp = Box¹(42); +//│ set tmp1 = H²(tmp, 1); +//│ return get¹(tmp1, true) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 42 + + +// A field typed with the top type admits the closure of a constructor fused into a match. +:deforest mono +:expect 42 +data class H(b: Any, n: Int) +private fun get(h: H, c: Bool): Int = if c then (if h.b is Box(v) then v) else h.n +get(H(Box(42), 1), true) +//│ deforest > >>> fusing >>> +//│ deforest > Box¹(42) -> +//│ deforest > match: scrut⁰ +//│ deforest > fields: scrut⁰.v¹ +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp: H³, $scrut_Box, $scrut_rest, $c_rest, tmp1: Function⁰, Box_v: Int⁰; +//│ @affine(1) @private +//│ define $scrut_Box as fun $scrut_Box⁰(Box_v1: Int⁰)(fv_arg$Box$0$: ?, fv_v: Int⁰): ? { +//│ set fv_arg$Box$0$ = Box_v1; +//│ set fv_v = fv_arg$Box$0$; +//│ return fv_v +//│ }; +//│ @private +//│ define $scrut_rest as fun $scrut_rest⁰(): ? { +//│ return $c_rest⁰() +//│ }; +//│ @private +//│ define $c_rest as fun $c_rest⁰(): ? { +//│ return null +//│ }; +//│ @private +//│ define get² as fun get³(h: H³, c: Bool⁰): Int⁰ { +//│ let v: Int⁰, arg$Box$0$: ?, scrut: Function⁰; +//│ match c +//│ true => +//│ set scrut = (h.b⁶ as! Function⁰); +//│ return (scrut(arg$Box$0$, v) as! Int⁰) +//│ else +//│ return h.n⁵ +//│ end +//│ }; +//│ @data +//│ define H⁴ as class H³ { +//│ val b⁷: Unknown; +//│ val n⁶: Int⁰; +//│ constructor H⁵(b: Unknown, n: Int⁰) { +//│ define b⁷ as val b⁶: Unknown = b; +//│ define n⁶ as val n⁵: Int⁰ = n; +//│ end +//│ } +//│ }; +//│ set Box_v = 42; +//│ set tmp1 = $scrut_Box⁰(Box_v); +//│ set tmp = H⁵(tmp1, 1); +//│ return get³(tmp, true) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 42 + + + + +// A fused value passing through a function whose code needs no rewriting still changes that function's types, +// so the instance it passes through gets its own copy: `idR$idR(x: Function)` +:deforest +:expect 42 +private fun idpoly(x) = x +private fun get(b) = if b is Box(v) then v +get(idpoly(Box(42))) +//│ deforest > >>> fusing >>> +//│ deforest > Box¹(42) -> +//│ deforest > match: b⁸ +//│ deforest > fields: b⁸.v¹ +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let get$b_Box, get$b_rest, get$get, idpoly$idpoly, tmp: Function⁰, tmp1: Function⁰, Box_v: Int⁰; +//│ @private @private +//│ define get$get as fun get$get⁰(b: Function⁰): Int⁰ { +//│ let v: Int⁰, arg$Box$0$: ?; +//│ return (b(arg$Box$0$, v) as! Int⁰) +//│ }; +//│ @private @private +//│ define idpoly$idpoly as fun idpoly$idpoly⁰(x: Function⁰): Function⁰ { +//│ return x +//│ }; +//│ @affine(1) @private +//│ define get$b_Box as fun get$b_Box⁰(Box_v1: Int⁰)(fv_arg$Box$0$: ?, fv_v: Int⁰): ? { +//│ set fv_arg$Box$0$ = Box_v1; +//│ set fv_v = fv_arg$Box$0$; +//│ return fv_v +//│ }; +//│ @private +//│ define get$b_rest as fun get$b_rest⁰(): ? { +//│ return null +//│ }; +//│ @private +//│ define idpoly⁰ as fun idpoly¹(x: Box²): Box² { +//│ return x +//│ }; +//│ @private +//│ define get⁴ as fun get⁵(b: Box²): Int⁰ { +//│ let v: Int⁰, arg$Box$0$: ?; +//│ match b +//│ Box² => +//│ set arg$Box$0$ = b.v¹; +//│ set v = arg$Box$0$; +//│ return v +//│ else +//│ throw new globalThis⁰.Error⁰("match error") +//│ end +//│ }; +//│ set Box_v = 42; +//│ set tmp = get$b_Box⁰(Box_v); +//│ set tmp1 = idpoly$idpoly⁰(tmp); +//│ return get$get⁰(tmp1) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 42 + +// Each instance gets the types of what passes through it: `Function` for the match and `Int` for the selection +:deforest +:expect 43 +private fun idpoly(x) = x +private fun get(b) = if b is Box(v) then v +get(idpoly(Box(42))) + idpoly(Box(1)).v +//│ deforest > >>> fusing >>> +//│ deforest > Box¹(1) -> +//│ deforest > select: v +//│ deforest > Box¹(42) -> +//│ deforest > match: b⁹ +//│ deforest > fields: b⁹.v¹ +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp: Int⁰, get$b_Box, get$b_rest, get$get, idpoly$idpoly, idpoly$idpoly1, tmp1: Function⁰, tmp2: Function⁰, tmp3: Int⁰, tmp4: Int⁰, Box_v: Int⁰, Box_v1: Int⁰; +//│ @private @private +//│ define get$get as fun get$get¹(b: Function⁰): Int⁰ { +//│ let v: Int⁰, arg$Box$0$: ?; +//│ return (b(arg$Box$0$, v) as! Int⁰) +//│ }; +//│ @private @private +//│ define idpoly$idpoly as fun idpoly$idpoly¹(x: Function⁰): Function⁰ { +//│ return x +//│ }; +//│ @private @private +//│ define idpoly$idpoly1 as fun idpoly$idpoly²(x: Int⁰): Int⁰ { +//│ return x +//│ }; +//│ @affine(1) @private +//│ define get$b_Box as fun get$b_Box¹(Box_v2: Int⁰)(fv_arg$Box$0$: ?, fv_v: Int⁰): ? { +//│ set fv_arg$Box$0$ = Box_v2; +//│ set fv_v = fv_arg$Box$0$; +//│ return fv_v +//│ }; +//│ @private +//│ define get$b_rest as fun get$b_rest¹(): ? { +//│ return null +//│ }; +//│ @private +//│ define idpoly² as fun idpoly³(x: Box²): Box² { +//│ return x +//│ }; +//│ @private +//│ define get⁶ as fun get⁷(b: Box²): Int⁰ { +//│ let v: Int⁰, arg$Box$0$: ?; +//│ match b +//│ Box² => +//│ set arg$Box$0$ = b.v¹; +//│ set v = arg$Box$0$; +//│ return v +//│ else +//│ throw new globalThis⁰.Error⁰("match error") +//│ end +//│ }; +//│ set Box_v = 42; +//│ set tmp1 = get$b_Box¹(Box_v); +//│ set tmp2 = idpoly$idpoly¹(tmp1); +//│ set tmp = get$get¹(tmp2); +//│ set Box_v1 = 1; +//│ set tmp3 = Box_v1; +//│ set tmp4 = idpoly$idpoly²(tmp3); +//│ return +⁰(tmp, tmp4) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 43 + + +// A fused value passing through nested functions gets copies along the whole path: `idTwice$idTwice` calls +// its own copies of `idA` +:deforest +:expect 42 +private fun idpoly(x) = x +private fun idTwice(x) = idpoly(idpoly(x)) +private fun get(b) = if b is Box(v) then v +get(idTwice(Box(42))) +//│ deforest > >>> fusing >>> +//│ deforest > Box¹(42) -> +//│ deforest > match: b¹⁰ +//│ deforest > fields: b¹⁰.v¹ +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let get$b_Box, get$b_rest, get$get, idTwice$idTwice, idTwice_idpoly$idpoly, idTwice_idpoly$idpoly1, tmp: Function⁰, tmp1: Function⁰, Box_v: Int⁰; +//│ @private @private +//│ define get$get as fun get$get²(b: Function⁰): Int⁰ { +//│ let v: Int⁰, arg$Box$0$: ?; +//│ return (b(arg$Box$0$, v) as! Int⁰) +//│ }; +//│ @private @private +//│ define idTwice$idTwice as fun idTwice$idTwice⁰(x: Function⁰): Function⁰ { +//│ let tmp2: Function⁰; +//│ set tmp2 = idTwice_idpoly$idpoly⁰(x); +//│ return idTwice_idpoly$idpoly¹(tmp2) +//│ }; +//│ @private @private +//│ define idTwice_idpoly$idpoly as fun idTwice_idpoly$idpoly¹(x: Function⁰): Function⁰ { +//│ return x +//│ }; +//│ @private @private +//│ define idTwice_idpoly$idpoly1 as fun idTwice_idpoly$idpoly⁰(x: Function⁰): Function⁰ { +//│ return x +//│ }; +//│ @affine(1) @private +//│ define get$b_Box as fun get$b_Box²(Box_v1: Int⁰)(fv_arg$Box$0$: ?, fv_v: Int⁰): ? { +//│ set fv_arg$Box$0$ = Box_v1; +//│ set fv_v = fv_arg$Box$0$; +//│ return fv_v +//│ }; +//│ @private +//│ define get$b_rest as fun get$b_rest²(): ? { +//│ return null +//│ }; +//│ @private +//│ define idpoly⁴ as fun idpoly⁵(x: Box²): Box² { +//│ return x +//│ }; +//│ @private +//│ define idTwice⁰ as fun idTwice¹(x: Box²): Box² { +//│ let tmp2: Box²; +//│ set tmp2 = idpoly⁵(x); +//│ return idpoly⁵(tmp2) +//│ }; +//│ @private +//│ define get⁸ as fun get⁹(b: Box²): Int⁰ { +//│ let v: Int⁰, arg$Box$0$: ?; +//│ match b +//│ Box² => +//│ set arg$Box$0$ = b.v¹; +//│ set v = arg$Box$0$; +//│ return v +//│ else +//│ throw new globalThis⁰.Error⁰("match error") +//│ end +//│ }; +//│ set Box_v = 42; +//│ set tmp = get$b_Box²(Box_v); +//│ set tmp1 = idTwice$idTwice⁰(tmp); +//│ return get$get²(tmp1) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 42 + + +// A temp var for field for a fused constructor is typed by the flow, not by the field's declaration: `O_b` is +// `Box`, as only a `Box` flows there +:deforest +:expect Box(42) +data class O(b: Object) +private fun get(h) = h.O#b +get(O(Box(42))) +//│ deforest > >>> fusing >>> +//│ deforest > O²(tmp⁰) -> +//│ deforest > select: b +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp: Box², get$get, tmp1: Box², O_b: Box²; +//│ @private @private +//│ define get$get as fun get$get³(h: Box²): Object⁰ { +//│ return h +//│ }; +//│ @private +//│ define get¹⁰ as fun get¹¹(h: O³): Object⁰ { +//│ return h.b¹¹ +//│ }; +//│ @data +//│ define O⁴ as class O³ { +//│ val b¹²: Object⁰; +//│ constructor O²(b: Object⁰) { +//│ define b¹² as val b¹¹: Object⁰ = b; +//│ end +//│ } +//│ }; +//│ set tmp = Box¹(42); +//│ set O_b = tmp; +//│ set tmp1 = O_b; +//│ return get$get³(tmp1) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = Box(42) + + + +// A cast proven to be safe by flow is removed if any of its sources is fused, +// as the closure replacing it would fail the check. +// and the slots that both the fused and unfused sources flow into are retyped to admit both (test_g$g returns Object) +:deforest +:expect 1 +private fun cons(x) = if x is Box(v) then v +private fun g(x: Object): Box = x +private fun test(c, d) = + let fused = Box(0) + let notFused = Box(1) + if c then cons(fused) + else + g(if d then fused else notFused) + 1 +test(false, true) +//│ deforest > >>> fusing >>> +//│ deforest > Box¹(0) -> +//│ deforest > match: x¹ +//│ deforest > fields: x¹.v¹ +//│ deforest > Box¹(0) -> +//│ deforest > match: x¹ +//│ deforest > fields: x¹.v¹ +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let test_cons$x_Box, test_cons$x_rest, test_cons$x_Box1, test_cons$x_rest1, test_cons$cons, test$test, test_cons$cons1, test_g$g, test_g$g1; +//│ @private @private +//│ define test_cons$cons as fun test_cons$cons⁰(x: Function⁰): Int⁰ { +//│ let v: Int⁰, arg$Box$0$: ?; +//│ return (x(arg$Box$0$, v) as! Int⁰) +//│ }; +//│ @private @private +//│ define test$test as fun test$test⁰(c: Bool⁰, d: Bool⁰): Int⁰ { +//│ let notFused: Box², fused: Function⁰, tmp: Object⁰, Box_v: Int⁰; +//│ set Box_v = 0; +//│ set fused = test_cons$x_Box⁰(Box_v); +//│ set notFused = Box¹(1); +//│ match c +//│ true => +//│ return test_cons$cons¹(fused) +//│ else +//│ match d +//│ true => +//│ set tmp = fused; +//│ end +//│ else +//│ set tmp = notFused; +//│ end +//│ do test_g$g⁰(tmp); +//│ return 1 +//│ end +//│ }; +//│ @private @private +//│ define test_cons$cons1 as fun test_cons$cons¹(x: Function⁰): Int⁰ { +//│ let v: Int⁰, arg$Box$0$: ?; +//│ return (x(arg$Box$0$, v) as! Int⁰) +//│ }; +//│ @private @private +//│ define test_g$g as fun test_g$g⁰(x: Object⁰): Object⁰ { +//│ return x +//│ }; +//│ @private @private +//│ define test_g$g1 as fun test_g$g¹(x: Object⁰): Object⁰ { +//│ return x +//│ }; +//│ @affine(1) @private +//│ define test_cons$x_Box as fun test_cons$x_Box¹(Box_v: Int⁰)(fv_arg$Box$0$: ?, fv_v: Int⁰): ? { +//│ set fv_arg$Box$0$ = Box_v; +//│ set fv_v = fv_arg$Box$0$; +//│ return fv_v +//│ }; +//│ @affine(1) @private +//│ define test_cons$x_Box1 as fun test_cons$x_Box⁰(Box_v: Int⁰)(fv_arg$Box$0$: ?, fv_v: Int⁰): ? { +//│ set fv_arg$Box$0$ = Box_v; +//│ set fv_v = fv_arg$Box$0$; +//│ return fv_v +//│ }; +//│ @private +//│ define test_cons$x_rest as fun test_cons$x_rest⁰(): ? { +//│ return null +//│ }; +//│ @private +//│ define test_cons$x_rest1 as fun test_cons$x_rest¹(): ? { +//│ return null +//│ }; +//│ @private +//│ define cons⁰ as fun cons¹(x: Box²): Int⁰ { +//│ let v: Int⁰, arg$Box$0$: ?; +//│ match x +//│ Box² => +//│ set arg$Box$0$ = x.v¹; +//│ set v = arg$Box$0$; +//│ return v +//│ else +//│ throw new globalThis⁰.Error⁰("match error") +//│ end +//│ }; +//│ @private +//│ define g⁰ as fun g¹(x: Object⁰): Box² { +//│ return (x as! Box²) +//│ }; +//│ @private +//│ define test⁰ as fun test¹(c: Bool⁰, d: Bool⁰): Int⁰ { +//│ let notFused: Box², tmp: Object⁰, fused: Function⁰, Box_v: Int⁰; +//│ set Box_v = 0; +//│ set fused = test_cons$x_Box¹(Box_v); +//│ set notFused = Box¹(1); +//│ match c +//│ true => +//│ return test_cons$cons⁰(fused) +//│ else +//│ match d +//│ true => +//│ set tmp = fused; +//│ end +//│ else +//│ set tmp = notFused; +//│ end +//│ do test_g$g¹(tmp); +//│ return 1 +//│ end +//│ }; +//│ return test$test⁰(false, true) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 1 + + + +// `tmp2` (the result for `if d then fused else notFused`) is typed as A, and (notFused: Object) needs to +// be casted before assigned to `tmp2` +:expect 1 +:deforest +data class A(n: Int) +data class O(b: A) +private fun up(x: Object): Object = x +private fun get(h) = h.O#b +private fun g(x) = x +private fun test(c, d) = + let fused = O(A(0)) + let notFused = up(A(1)) + if c then get(fused) + else + g(if d then fused else notFused) + 1 +test(false, true) +//│ deforest > >>> fusing >>> +//│ deforest > O⁵(tmp¹) -> +//│ deforest > select: b +//│ deforest > O⁵(tmp¹) -> +//│ deforest > select: b +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let test_get$get, test_get$get1, test$test, test_g$g, test_g$g1; +//│ @private @private +//│ define test_get$get as fun test_get$get⁰(h: A⁰): A⁰ { +//│ return h +//│ }; +//│ @private @private +//│ define test_get$get1 as fun test_get$get¹(h: A⁰): A⁰ { +//│ return h +//│ }; +//│ @private @private +//│ define test$test as fun test$test¹(c: Bool⁰, d: Bool⁰): Unknown { +//│ let notFused: Object⁰, tmp: A⁰, tmp1: A⁰, fused: A⁰, tmp2: A⁰, O_b: A⁰; +//│ set tmp = A¹(0); +//│ set O_b = tmp; +//│ set fused = O_b; +//│ set tmp1 = A¹(1); +//│ set notFused = up⁰(tmp1); +//│ match c +//│ true => +//│ return test_get$get¹(fused) +//│ else +//│ match d +//│ true => +//│ set tmp2 = fused; +//│ end +//│ else +//│ set tmp2 = (notFused as! A⁰); +//│ end +//│ do test_g$g²(tmp2); +//│ return 1 +//│ end +//│ }; +//│ @private @private +//│ define test_g$g as fun test_g$g²(x: A⁰): A⁰ { +//│ return x +//│ }; +//│ @private @private +//│ define test_g$g1 as fun test_g$g³(x: A⁰): A⁰ { +//│ return x +//│ }; +//│ @private +//│ define up¹ as fun up⁰(x: Object⁰): Object⁰ { +//│ return x +//│ }; +//│ @private +//│ define get¹² as fun get¹³(h: O⁶): A⁰ { +//│ return h.b¹³ +//│ }; +//│ @private +//│ define g² as fun g³(x: Object⁰): Object⁰ { +//│ return x +//│ }; +//│ @private +//│ define test² as fun test³(c: Bool⁰, d: Bool⁰): Unknown { +//│ let notFused: Object⁰, tmp: A⁰, tmp1: A⁰, tmp2: A⁰, fused: A⁰, O_b: A⁰; +//│ set tmp = A¹(0); +//│ set O_b = tmp; +//│ set fused = O_b; +//│ set tmp1 = A¹(1); +//│ set notFused = up⁰(tmp1); +//│ match c +//│ true => +//│ return test_get$get⁰(fused) +//│ else +//│ match d +//│ true => +//│ set tmp2 = fused; +//│ end +//│ else +//│ set tmp2 = (notFused as! A⁰); +//│ end +//│ do test_g$g³(tmp2); +//│ return 1 +//│ end +//│ }; +//│ @data +//│ define A² as class A⁰ { +//│ val n⁷: Int⁰; +//│ constructor A¹(n: Int⁰) { +//│ define n⁷ as val n⁸: Int⁰ = n; +//│ end +//│ } +//│ }; +//│ @data +//│ define O⁷ as class O⁶ { +//│ val b¹⁴: A⁰; +//│ constructor O⁵(b: A⁰) { define b¹⁴ as val b¹³: A⁰ = b; end } +//│ }; +//│ return test$test¹(false, true) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 1 + + +// `a.AA#x` has no concrete lower bound and doesn't block fusion, and it flows into a slot +// retyped by fusion from A to Function. As `a.AA#x` has type A, unrelated to Function, it can never be produced: +// as it is pure, it is dropped, and `undefined` is passed in its place +:expect 0 +object A +data class AA(x: A) +private fun f(a, b) = if a is AA then f(a, a.AA#x) else if b is A then 0 +f(A, A) +//│ deforest > >>> fusing >>> +//│ deforest > A³ -> +//│ deforest > match: b¹⁵ +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let f$b_A, f$b_rest, f$a_rest, f$f, deforest$lam: Function⁰; +//│ @private @private +//│ define f$f as fun f$f⁰(a: A³, b: Function⁰): Int⁰ { +//│ match a +//│ AA⁰ => +//│ return f$f⁰(a, (undefined as!! Function⁰)) +//│ else +//│ return (b() as! Int⁰) +//│ end +//│ }; +//│ @affine(1) @private +//│ define f$b_A as fun f$b_A⁰()(): ? { +//│ return 0 +//│ }; +//│ @private +//│ define f$b_rest as fun f$b_rest⁰(): ? { +//│ return f$a_rest⁰() +//│ }; +//│ @private +//│ define f$a_rest as fun f$a_rest⁰(): ? { +//│ return null +//│ }; +//│ @private +//│ define f⁰ as fun f¹(a: A³, b: A³): Int⁰ { +//│ match a +//│ AA⁰ => +//│ return f¹(a, a.x²) +//│ else +//│ match b +//│ A³ => +//│ return 0 +//│ else +//│ throw new globalThis⁰.Error⁰("match error") +//│ end +//│ end +//│ }; +//│ define A⁴ as object A³; +//│ @data +//│ define AA¹ as class AA⁰ { +//│ val x³: A³; +//│ constructor AA²(x: A³) { +//│ define x³ as val x²: A³ = x; +//│ end +//│ } +//│ }; +//│ set deforest$lam = f$b_A⁰(); +//│ return f$f⁰(A³, deforest$lam) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 0 + + + +// `fail()` has no concrete lower bound but still runs (it throws), and it is returned from `g`, whose result +// is retyped by fusion from A to Function. As `fail()` has type A, unrelated to Function, it never returns: +// it is still called, and `undefined` is returned in its place +:re +object A +private fun fail(): A = throw Error("error") +private fun g(c) = if c then A else fail() +private fun f(a) = if a is A then 0 else 1 +[f(g(true)), f(g(false))] +//│ deforest > >>> fusing >>> +//│ deforest > A⁵ -> +//│ deforest > match: a² +//│ deforest > A⁵ -> +//│ deforest > match: a² +//│ deforest > <<< fusing <<< +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp: Int⁰, tmp1: Int⁰, f$a_A, f$a_rest, f$a_A1, f$a_rest1, g$g, f$f, g$g1, f$f1, tmp2: Function⁰, tmp3: Function⁰; +//│ @private @private +//│ define g$g as fun g$g⁰(c: Bool⁰): Function⁰ { +//│ match c +//│ true => +//│ return f$a_A⁰() +//│ else +//│ do fail⁰(); +//│ return (undefined as!! Function⁰) +//│ end +//│ }; +//│ @private @private +//│ define f$f as fun f$f¹(a: Function⁰): Int⁰ { +//│ return (a() as! Int⁰) +//│ }; +//│ @private @private +//│ define g$g1 as fun g$g¹(c: Bool⁰): Function⁰ { +//│ match c +//│ true => +//│ return f$a_A¹() +//│ else +//│ do fail⁰(); +//│ return (undefined as!! Function⁰) +//│ end +//│ }; +//│ @private @private +//│ define f$f1 as fun f$f²(a: Function⁰): Int⁰ { +//│ return (a() as! Int⁰) +//│ }; +//│ @affine(1) @private +//│ define f$a_A as fun f$a_A⁰()(): ? { +//│ return 0 +//│ }; +//│ @affine(1) @private +//│ define f$a_A1 as fun f$a_A¹()(): ? { +//│ return 0 +//│ }; +//│ @private +//│ define f$a_rest as fun f$a_rest¹(): ? { +//│ return null +//│ }; +//│ @private +//│ define f$a_rest1 as fun f$a_rest²(): ? { +//│ return null +//│ }; +//│ @private +//│ define fail¹ as fun fail⁰(): A⁵ { +//│ throw globalThis⁰.Error¹("error") +//│ }; +//│ @private +//│ define g⁴ as fun g⁵(c: Bool⁰): A⁵ { +//│ match c +//│ true => +//│ return A⁵ +//│ else +//│ return fail⁰() +//│ end +//│ }; +//│ @private +//│ define f² as fun f³(a: A⁵): Int⁰ { +//│ match a +//│ A⁵ => +//│ return 0 +//│ else +//│ return 1 +//│ end +//│ }; +//│ define A⁶ as object A⁵; +//│ set tmp2 = g$g¹(true); +//│ set tmp = f$f²(tmp2); +//│ set tmp3 = g$g⁰(false); +//│ set tmp1 = f$f¹(tmp3); +//│ return [tmp, tmp1] +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ ═══[RUNTIME ERROR] Error: error diff --git a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls index 9d65550750..18843b9366 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/eta-expansion/recursive.mls @@ -80,7 +80,7 @@ sum(enum(5), 0) //│ continue loopLabel //│ end //│ unreachable /* Rest of abortive labelled block */ -//│ return inlinedVal +//│ return (inlinedVal as! Int⁰) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 15 @@ -114,7 +114,7 @@ len(flatten(id((1 :: 2 :: Nil) :: (3 :: Nil) :: Nil))) //│ eta-expansion > flatten$flatten⁰#0 @ : [1, 5] //│ eta-expansion > <<< eta-expansion targets shapes <<< //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let tmp, tmp1, tmp2, tmp3, tmp4, flatten$flatten, flatten_append$append$worker, flatten$flatten$worker; +//│ let tmp, tmp1, tmp2, tmp3, tmp4, flatten$flatten, flatten_append$append$worker, flatten$flatten$worker, inlinedVal; //│ @inline @private //│ define flatten$flatten as fun flatten$flatten⁰(xss)(eta$0$1, eta$0$2, eta$0$4) { //│ return flatten$flatten$worker⁰(xss, eta$0$1, eta$0$2, eta$0$4) @@ -142,10 +142,11 @@ len(flatten(id((1 :: 2 :: Nil) :: (3 :: Nil) :: Nil))) //│ Nil⁰ => //│ return ys(eta$0$1, eta$0$2, eta$0$4) //│ Cons¹ => -//│ let inlinedVal; +//│ let inlinedVal1, inlinedVal2; //│ set arg$Cons$1$ = xs.t⁰; -//│ set inlinedVal = flatten_append$append$worker⁰(arg$Cons$1$, ys, undefined, undefined, undefined); -//│ return +⁰(1, inlinedVal) +//│ set inlinedVal2 = flatten_append$append$worker⁰(arg$Cons$1$, ys, undefined, undefined, undefined); +//│ set inlinedVal1 = (inlinedVal2 as! Int⁰); +//│ return +⁰(1, inlinedVal1) //│ else //│ throw new globalThis⁰.Error⁰("match error") //│ end @@ -196,7 +197,8 @@ len(flatten(id((1 :: 2 :: Nil) :: (3 :: Nil) :: Nil))) //│ set tmp2 = Cons⁰(3, Nil⁰); //│ set tmp3 = Cons⁰(tmp2, Nil⁰); //│ set tmp4 = Cons⁰(tmp1, tmp3); -//│ return flatten$flatten$worker⁰(tmp4, undefined, undefined, undefined) +//│ set inlinedVal = flatten$flatten$worker⁰(tmp4, undefined, undefined, undefined); +//│ return (inlinedVal as! Int⁰) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 3 diff --git a/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls b/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls index 28a1dc5a5e..65013dd1f1 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/fusibility.mls @@ -551,7 +551,7 @@ c(mk(7)) //│ let tmp1, tmp2; //│ set tmp1 = g¹(p)(); //│ set tmp2 = g¹(p)(); -//│ return +⁰(tmp1, tmp2) +//│ return (+⁰(tmp1, tmp2) as! Int⁰) //│ }; //│ set tmp = mk¹(7); //│ return c¹(tmp) @@ -654,7 +654,7 @@ c(mk(7)) //│ let tmp1, tmp2; //│ set tmp1 = g³(p)(0)(); //│ set tmp2 = g³(p)(0)(); -//│ return +⁰(tmp1, tmp2) +//│ return (+⁰(tmp1, tmp2) as! Int⁰) //│ }; //│ set tmp = mk³(7); //│ return c³(tmp) @@ -735,7 +735,7 @@ c(mk(7)) //│ define c⁴ as fun c⁵(p) { //│ let lambda$here; //│ set lambda$here = lambda$⁰(p); -//│ return callTwice⁰(lambda$here) +//│ return (callTwice⁰(lambda$here) as! Int⁰) //│ }; //│ set tmp = mk⁵(7); //│ return c⁵(tmp) diff --git a/hkmc2/shared/src/test/mlscript/deforest/recursive.mls b/hkmc2/shared/src/test/mlscript/deforest/recursive.mls index a149e5c916..2dbd992211 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/recursive.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/recursive.mls @@ -973,10 +973,10 @@ f(3) //│ deforest > fields: a⁸.x⁹ //│ deforest > <<< fusing <<< //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let f$g, f$f, f$g1, f$a_A, f$a_A1; +//│ let f$a_A, f$a_A1, f$g, f$f, f$g1; //│ @private //│ define f$g as fun f$g⁰(a) { -//│ return a(undefined, undefined, undefined) +//│ return (a(undefined, undefined, undefined) as! Int⁰) //│ }; //│ @private //│ define f$f as fun f$f⁰(x) { @@ -992,7 +992,7 @@ f(3) //│ }; //│ @private //│ define f$g1 as fun f$g¹(a) { -//│ return a(undefined, undefined, undefined) +//│ return (a(undefined, undefined, undefined) as! Int⁰) //│ }; //│ @affine(1) @private //│ define f$a_A as fun f$a_A¹(A_x)(fv_arg$A$0$, fv_y, fv_tmp) { diff --git a/hkmc2/shared/src/test/mlscript/deforest/relaxedPrograms.mls b/hkmc2/shared/src/test/mlscript/deforest/relaxedPrograms.mls index b6ea877ae8..c1cf36a47c 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/relaxedPrograms.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/relaxedPrograms.mls @@ -130,7 +130,7 @@ fun loopSibling() = //│ deforest > match: scrut⁶ //│ deforest > fields: scrut⁶.x¹ //│ deforest > <<< fusing <<< -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Rewrite.scala:404': unsupported `continue` to label:lbl during rewriting +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Rewrite.scala:530': unsupported `continue` to label:lbl during rewriting //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. diff --git a/hkmc2/shared/src/test/mlscript/deforest/simple.mls b/hkmc2/shared/src/test/mlscript/deforest/simple.mls index 228a83f51c..931e8c4ede 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/simple.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/simple.mls @@ -558,10 +558,10 @@ f(id(A)) + f(A) //│ deforest > match: x²² //│ deforest > <<< fusing <<< //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ let tmp, tmp1, tmp2, f$f, f$x_A, f$x_rest, deforest$lam; +//│ let tmp, tmp1, tmp2, f$x_A, f$x_rest, f$f, deforest$lam; //│ @private //│ define f$f as fun f$f⁰(x) { -//│ return x() +//│ return (x() as! Int⁰) //│ }; //│ @affine(1) @private //│ define f$x_A as fun f$x_A⁰()() { @@ -580,7 +580,7 @@ f(id(A)) + f(A) //│ end //│ }; //│ set tmp = Predef⁰.id⁰(A⁰); -//│ set tmp1 = f¹(tmp); +//│ set tmp1 = f¹((tmp as! A⁰)); //│ set deforest$lam = f$x_A⁰(); //│ set tmp2 = f$f⁰(deforest$lam); //│ return +⁰(tmp1, tmp2) diff --git a/hkmc2/shared/src/test/mlscript/deforest/todos.mls b/hkmc2/shared/src/test/mlscript/deforest/todos.mls index a339fd9f62..022ce67b94 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/todos.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/todos.mls @@ -48,19 +48,19 @@ test(p) + f(p) + test(AA(AA(AA(10)))) + test(B) //│ deforest > fields: arg$AA$0$¹.x¹ //│ deforest > <<< fusing <<< //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ let tmp, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, tmp8, tmp9, tmp10, test$test, test$test1, test$x_dflt, test$x_rest, test$x_AA, test$x_rest1, test$arg$AA$0$_AA, test$arg$AA$0$_rest, test$arg$AA$0$_AA1, test$arg$AA$0$_rest1, AA_x, AA_x1, AA_x2, deforest$lam; +//│ let tmp, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, test$x_B, test$x_rest, test$x_AA, test$x_rest1, test$arg$AA$0$_AA, test$arg$AA$0$_rest, test$arg$AA$0$_AA1, test$arg$AA$0$_rest1, test$test, test$test1, tmp8, tmp9, tmp10, AA_x, AA_x1, AA_x2, deforest$lam; //│ @private //│ define test$test as fun test$test⁰(x) { //│ let t, a, arg$AA$0$, arg$AA$0$1, arg$AA$0$2, tmp11, tmp12, tmp13, tmp14; -//│ return x(tmp11, t, tmp12, tmp13, tmp14) +//│ return (x(tmp11, t, tmp12, tmp13, tmp14) as! Int⁰) //│ }; //│ @private //│ define test$test1 as fun test$test¹(x) { -//│ let t, a, arg$AA$0$, arg$AA$0$1, arg$AA$0$2, tmp11, tmp12, tmp13, tmp14; -//│ return x(arg$AA$0$, arg$AA$0$1, arg$AA$0$2, a, tmp11, t, tmp12, tmp13, tmp14) +//│ let t, a, arg$AA$0$, tmp11, tmp12, tmp13, tmp14, arg$AA$0$1, arg$AA$0$2; +//│ return (x(arg$AA$0$1, arg$AA$0$2, arg$AA$0$, a, tmp11, t, tmp12, tmp13, tmp14) as! Int⁰) //│ }; //│ @affine(1) @private -//│ define test$x_dflt as fun test$x_dflt⁰()(fv_tmp, fv_t, fv_tmp1, fv_tmp2, fv_tmp3) { +//│ define test$x_B as fun test$x_B⁰()(fv_tmp, fv_t, fv_tmp1, fv_tmp2, fv_tmp3) { //│ set fv_tmp = 4; //│ return test$x_rest⁰(fv_t, fv_tmp, fv_tmp1, fv_tmp2, fv_tmp3) //│ }; @@ -158,16 +158,16 @@ test(p) + f(p) + test(AA(AA(AA(10)))) + test(B) //│ set tmp3 = f¹(p⁰); //│ set tmp4 = +⁰(tmp2, tmp3); //│ set AA_x = 10; -//│ set tmp5 = test$arg$AA$0$_AA¹(AA_x); -//│ set AA_x1 = tmp5; -//│ set tmp6 = test$arg$AA$0$_AA⁰(AA_x1); -//│ set AA_x2 = tmp6; -//│ set tmp7 = test$x_AA⁰(AA_x2); -//│ set tmp8 = test$test¹(tmp7); -//│ set tmp9 = +⁰(tmp4, tmp8); -//│ set deforest$lam = test$x_dflt⁰(); -//│ set tmp10 = test$test⁰(deforest$lam); -//│ return +⁰(tmp9, tmp10) +//│ set tmp8 = test$arg$AA$0$_AA¹(AA_x); +//│ set AA_x1 = tmp8; +//│ set tmp9 = test$arg$AA$0$_AA⁰(AA_x1); +//│ set AA_x2 = tmp9; +//│ set tmp10 = test$x_AA⁰(AA_x2); +//│ set tmp5 = test$test¹(tmp10); +//│ set tmp6 = +⁰(tmp4, tmp5); +//│ set deforest$lam = test$x_B⁰(); +//│ set tmp7 = test$test⁰(deforest$lam); +//│ return +⁰(tmp6, tmp7) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 78 //│ p = AA(AA(AA(10))) @@ -234,7 +234,7 @@ f(D(1)) //│ deforest > match: d⁰ //│ deforest > fields: d⁰.x³ //│ deforest > <<< fusing <<< -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Rewrite.scala:269': deforestation copying the definition O +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Rewrite.scala:299': deforestation copying the definition O //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. //│ /!!!\ Uncaught error: java.lang.AssertionError: assertion failed: BlockMemberSymbol member:O for ClsLikeDefn is a free variable for this block @@ -252,7 +252,7 @@ E(y).get() //│ deforest > AA⁰(0) -> //│ deforest > match: scrut⁰ //│ deforest > <<< fusing <<< -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Rewrite.scala:269': deforestation copying the definition E +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'Rewrite.scala:299': deforestation copying the definition E //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. //│ /!!!\ Uncaught error: java.lang.AssertionError: assertion failed: BlockMemberSymbol member:E for ClsLikeDefn is a free variable for this block diff --git a/hkmc2/shared/src/test/mlscript/handlers/CurriedFunctionCalls.mls b/hkmc2/shared/src/test/mlscript/handlers/CurriedFunctionCalls.mls index 0437cadca1..17465392e9 100644 --- a/hkmc2/shared/src/test/mlscript/handlers/CurriedFunctionCalls.mls +++ b/hkmc2/shared/src/test/mlscript/handlers/CurriedFunctionCalls.mls @@ -79,7 +79,7 @@ fun g(a)(b) = f(a)(b) + f(b)(a) //│ match pc //│ 1 => //│ set tmp1 = runtime⁰.resumeValue﹖; -//│ return +⁰(tmp, tmp1) +//│ return (+⁰(tmp, tmp1) as! Int⁰) //│ else //│ end //│ end diff --git a/hkmc2/shared/src/test/mlscript/invalml/InvalMLCheck.mls b/hkmc2/shared/src/test/mlscript/invalml/InvalMLCheck.mls index 52da5b1dba..155ff79eb2 100644 --- a/hkmc2/shared/src/test/mlscript/invalml/InvalMLCheck.mls +++ b/hkmc2/shared/src/test/mlscript/invalml/InvalMLCheck.mls @@ -38,7 +38,9 @@ fun bar(x) = let t = x + 1 in "aaa" :e fun baz: Int -> Int fun baz(x) = "bbb" -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ║ l.40: fun baz(x) = "bbb" +//│ ╙── ^^^^^ //│ ╔══[COMPILATION ERROR] Type error in string literal with expected type Int //│ ║ l.40: fun baz(x) = "bbb" //│ ║ ^^^^^ @@ -54,7 +56,7 @@ add(1, 2) as Int :e add(0, 0) as Str //│ ╔══[COMPILATION ERROR] Type error in application of type ‹not implemented› with expected type Str -//│ ║ l.55: add(0, 0) as Str +//│ ║ l.57: add(0, 0) as Str //│ ║ ^^^^^^^^^ //│ ╙── because: cannot constrain Int <: Str //│ Type: Str @@ -62,7 +64,7 @@ add(0, 0) as Str :e fun errAdd(x: Int) = x + "1" //│ ╔══[COMPILATION ERROR] Type error in string literal with expected type Int -//│ ║ l.63: fun errAdd(x: Int) = x + "1" +//│ ║ l.65: fun errAdd(x: Int) = x + "1" //│ ║ ^^^ //│ ╙── because: cannot constrain Str <: Int @@ -82,11 +84,11 @@ high(x => x) :e high(x => x + 1) //│ ╔══[COMPILATION ERROR] Type error in reference with expected type Int -//│ ║ l.83: high(x => x + 1) +//│ ║ l.85: high(x => x + 1) //│ ║ ^ //│ ╙── because: cannot constrain A <: Int //│ ╔══[COMPILATION ERROR] Type error in application with expected type A -//│ ║ l.83: high(x => x + 1) +//│ ║ l.85: high(x => x + 1) //│ ║ ^^^^^ //│ ╙── because: cannot constrain Int <: A //│ Type: Int @@ -110,7 +112,7 @@ fun baz(z) = :e baz as Int -> (([A] -> A -> A), Int) -> Int //│ ╔══[COMPILATION ERROR] Cannot type non-function term Ref(member:baz) as (Int) ->{⊥} ([outer, 'A] -> ('A) ->{⊥} 'A, Int) ->{⊥} Int -//│ ║ l.111: baz as Int -> (([A] -> A -> A), Int) -> Int +//│ ║ l.113: baz as Int -> (([A] -> A -> A), Int) -> Int //│ ╙── ^^^ //│ Type: ⊥ @@ -121,7 +123,7 @@ baz(42) :e baz(42) as (([A] -> A -> A), Int) -> Int //│ ╔══[COMPILATION ERROR] Cannot type non-function term App(Ref(member:baz),Tup(List(Fld(‹›,Lit(IntLit(42)),None)))) as ([outer, 'A] -> ('A) ->{⊥} 'A, Int) ->{⊥} Int -//│ ║ l.122: baz(42) as (([A] -> A -> A), Int) -> Int +//│ ║ l.124: baz(42) as (([A] -> A -> A), Int) -> Int //│ ╙── ^^^^^^^ //│ Type: ⊥ @@ -142,10 +144,10 @@ id as [A] -> A -> A :e 42 as Int | Num //│ ╔══[COMPILATION ERROR] Unexpected term form in expression position (alternation) -//│ ║ l.143: 42 as Int | Num +//│ ║ l.145: 42 as Int | Num //│ ╙── ^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Term shape not yet supported by InvalML: CompType(Asc(Lit(IntLit(42)),Ref(member:Int)),Ref(member:Num),true) -//│ ║ l.143: 42 as Int | Num +//│ ║ l.145: 42 as Int | Num //│ ╙── ^^^^^^^^^^^^^^^ //│ Type: ⊥ @@ -176,7 +178,7 @@ foo :e foo(42, 1) //│ ╔══[COMPILATION ERROR] Type error in integer literal with expected type ¬'A -//│ ║ l.177: foo(42, 1) +//│ ║ l.179: foo(42, 1) //│ ║ ^ //│ ╟── because: cannot constrain Int <: ¬'A //│ ╟── because: cannot constrain 'A <: ¬{Int} diff --git a/hkmc2/shared/src/test/mlscript/lifter/FunInFun.mls b/hkmc2/shared/src/test/mlscript/lifter/FunInFun.mls index 9c8cab9a25..93432fd577 100644 --- a/hkmc2/shared/src/test/mlscript/lifter/FunInFun.mls +++ b/hkmc2/shared/src/test/mlscript/lifter/FunInFun.mls @@ -79,7 +79,7 @@ f(1, 2) //│ set g$here = g$⁰(used1, used2); //│ set foo = g$here; //│ set tmp = foo(used2); -//│ return +⁰(tmp, unused2) +//│ return (+⁰(tmp, unused2) as! Int⁰) //│ }; //│ return f¹(1, 2) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/lifter/Labels.mls b/hkmc2/shared/src/test/mlscript/lifter/Labels.mls index 6791ddddbd..6d037571bd 100644 --- a/hkmc2/shared/src/test/mlscript/lifter/Labels.mls +++ b/hkmc2/shared/src/test/mlscript/lifter/Labels.mls @@ -23,7 +23,7 @@ fun main() = //│ return x //│ }; //│ define main⁰ as fun main¹() { -//│ let scrut, tmp; +//│ let x, scrut, tmp; //│ block σ$x: //│ match 0 //│ true => @@ -38,9 +38,10 @@ fun main() = //│ do Predef⁰.print⁰("hi"); //│ set tmp = Predef⁰.print⁰("hi"); //│ end +//│ set x = (tmp as! Int⁰); //│ do Predef⁰.print⁰("bye"); //│ do Predef⁰.print⁰("bye"); -//│ return foo$⁰(tmp) +//│ return foo$⁰(x) //│ }; //│ end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/newres/Assignments.mls b/hkmc2/shared/src/test/mlscript/newres/Assignments.mls index 8a1e8515ad..d3a3beed23 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Assignments.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Assignments.mls @@ -9,7 +9,7 @@ module Later { class ItemClass { fun answer() = 42 } - mut val Item = 0 + mut val Item: Int = 0 } writeLater(7) @@ -58,8 +58,8 @@ replace(0) // Resolving an l-value must preserve receiver-before-RHS evaluation, once each. module Order { - mut val trace = "" - mut val value = 0 + mut val trace: Str = "" + mut val value: Int = 0 } fun receiver() = { set Order.trace = Order.trace + "receiver;" diff --git a/hkmc2/shared/src/test/mlscript/newres/CurriedFunctions.mls b/hkmc2/shared/src/test/mlscript/newres/CurriedFunctions.mls index 8a81488f3b..1004d62e1d 100644 --- a/hkmc2/shared/src/test/mlscript/newres/CurriedFunctions.mls +++ b/hkmc2/shared/src/test/mlscript/newres/CurriedFunctions.mls @@ -16,10 +16,19 @@ let f = foo(1)(2) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ f = 3 -:breakme // builtins like `+` do not yet yield a shape :e f(3) -//│ ═══[RUNTIME ERROR] TypeError: f is not a function +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.20: f(3) +//│ ║ ^^^^ +//│ ╟── Value of unknown shape cannot be called like a function. +//│ ╟── The shape of this value is unknown. +//│ ║ l.6: x + y +//│ ║ ^^^^^ +//│ ╟── Value of type 'Int' cannot be called like a function. +//│ ║ l.6: x + y +//│ ╙── ^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. :sr let f = foo(1) @@ -35,10 +44,19 @@ let g = f(2) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ g = 3 -:breakme // builtins like `+` do not yet yield a shape :e g(3) -//│ ═══[RUNTIME ERROR] TypeError: g is not a function +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.48: g(3) +//│ ║ ^^^^ +//│ ╟── Value of unknown shape cannot be called like a function. +//│ ╟── The shape of this value is unknown. +//│ ║ l.6: x + y +//│ ║ ^^^^^ +//│ ╟── Value of type 'Int' cannot be called like a function. +//│ ║ l.6: x + y +//│ ╙── ^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. let f = x => y => x + y diff --git a/hkmc2/shared/src/test/mlscript/newres/DeclarationLocations.mls b/hkmc2/shared/src/test/mlscript/newres/DeclarationLocations.mls index c777bc68fb..93db0cafa6 100644 --- a/hkmc2/shared/src/test/mlscript/newres/DeclarationLocations.mls +++ b/hkmc2/shared/src/test/mlscript/newres/DeclarationLocations.mls @@ -320,6 +320,15 @@ new Opened().f(1, 2) //│ ╟── Function 'f' expected 1 argument, but got 2 //│ ║ l.313: open fun f(x) = //│ ╙── ^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Open member 'f' is exposed to other compilation units, which can override it; annotate the types of its parameters and result +//│ ║ l.313: open fun f(x) = +//│ ║ ^ +//│ ╟── Parameter 'x' has no type annotation +//│ ║ l.313: open fun f(x) = +//│ ║ ^ +//│ ╟── Its result has no type annotation +//│ ║ l.313: open fun f(x) = +//│ ╙── ^ //│ = 3 // Each call cites the unknown parameter's declaration without repeating the callee span. @@ -329,22 +338,22 @@ fun invokeAtEachUse(f) = f() alias() //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.330: alias() +//│ ║ l.339: alias() //│ ║ ^^^^^^^ //│ ╟── Value of unknown shape cannot be called like a function. //│ ╟── Parameter 'f' admits values of unknown shape. -//│ ║ l.327: fun invokeAtEachUse(f) = +//│ ║ l.336: fun invokeAtEachUse(f) = //│ ║ ^ //│ ╟── 'invokeAtEachUse' is exposed by this compilation unit. -//│ ║ l.327: fun invokeAtEachUse(f) = +//│ ║ l.336: fun invokeAtEachUse(f) = //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.329: f() +//│ ║ l.338: f() //│ ║ ^^^ //│ ╟── Value of unknown shape cannot be called like a function. //│ ╟── Parameter 'f' admits values of unknown shape. -//│ ║ l.327: fun invokeAtEachUse(f) = +//│ ║ l.336: fun invokeAtEachUse(f) = //│ ║ ^ //│ ╟── 'invokeAtEachUse' is exposed by this compilation unit. -//│ ║ l.327: fun invokeAtEachUse(f) = +//│ ║ l.336: fun invokeAtEachUse(f) = //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/newres/Erasure.mls b/hkmc2/shared/src/test/mlscript/newres/Erasure.mls index dd75cd6a45..5fc4750388 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Erasure.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Erasure.mls @@ -52,3 +52,189 @@ module Right with //│ ╟── candidate: type alias //│ ║ l.42: type Number = Str //│ ╙── ^^^^^^^^^^^ + + +// Stored function values retain a value representation through bindings and record fields. +:noInline +:expect 42 +private fun increment(value) = value + 1 +let stored = increment +let record = {apply: stored} +record.apply(41) +//│ = 42 +//│ record = {apply: fun increment} +//│ stored = fun increment + + +// A class value in a binding is a constructor object, distinct from an instance. +:noInline +:expect 42 +private class Box(val value) +let factory = Box +factory(42).value +//│ = 42 +//│ factory = fun Box { class: class Box } + + +// Preserve curried physical parameter lists and partially applied function values. +:noInline +:expect 42 +private fun add(x)(y) = x + y +let plusOne = add(1) +plusOne(41) +//│ = 42 +//│ plusOne = fun + + +// Nested functions' returns do not contribute to the enclosing function's result. +:noInline +:expect 42 +private fun enclosing() = + fun inner() = return "inner" + inner() + return 42 +enclosing() +//│ = 42 + + +// Contextual declared shapes must retain their generic call-site substitutions. +:noInline +:expect [42, "text"] +private fun identity[A](value: A): A = value +private fun forward(value) = identity(value) +[forward(42), forward("text")] +//│ = [42, "text"] + + +// Unknown from an exported input absorbs concrete references without changing their ABI. +:noInline +:expect [42, "text"] +fun exported(value) = value +[exported(42), exported("text")] +//│ = [42, "text"] + + +// Named tuple fields have source slots even though resolution synthesizes their declarations. +:noInline +:expect 42 +let tuple = [answer: 42] +tuple.0.answer +//│ = 42 +//│ tuple = [{answer: 42}] + + +// Pattern bindings and lambda inputs also receive the observed runtime shapes. +:noInline +:expect 42 +let apply = (value) => if value is [head, ..tail] then head else 0 +apply([42, 1]) +//│ = 42 +//│ apply = fun + + +// A later worksheet block can add shapes without rewriting a compiled declaration's slot. +:noInline +fun reused(value) = value +reused(42) +//│ = 42 + +:expect "next" +reused("next") +//│ = "next" + + +// Imported source definitions keep the representation assigned by their defining unit. +:expect 36 +import "../../mlscript-compile/NamedFieldLibrary.mjs" +NamedFieldLibrary.select(NamedFieldLibrary.fields.0) +//│ = 36 + + +// Private constructor functions retain the inferred representation of their arguments and fields. +:noInline +:sir +:siret +:expect 42 +private class PrivateBox(val value) with + private constructor +private module PrivateBox with + fun make(value) = PrivateBox(value) +PrivateBox.make(42).value +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp: PrivateBox⁰; +//│ @private +//│ define PrivateBox¹ as class PrivateBox⁰ { +//│ val value⁰: Int⁰; +//│ constructor PrivateBox²(value: Int⁰) { +//│ define value⁰ as val value¹: Int⁰ = value; +//│ end +//│ } +//│ } +//│ module PrivateBox³ { +//│ method make⁰ = fun make¹(value1: Int⁰): PrivateBox⁰ { +//│ return PrivateBox²(value1) +//│ } +//│ }; +//│ set tmp = PrivateBox³.make¹(42); +//│ return tmp.value¹ +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 42 + + +#lang(0.3.x) + + +// Placeholder parameters stay untyped until erasure observes their arguments. +:noInline +:sir +:siret +:expect 42 +let increment = => _ + 1 +increment(41) +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let increment; +//│ @private +//│ define increment as fun increment⁰(_0: Int⁰): ? { +//│ return +⁰(_0, 1) +//│ }; +//│ set increment¹ = increment⁰; +//│ return increment¹(41) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 42 +//│ increment = fun increment + + +// A trailing argument's implicit lambda receives its caller's inferred parameter type. +:noInline +:sir +:siret +:expect 42 +private fun apply(value, f) = f(value) +apply(41) \ _ + 1 +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let lambda; +//│ @private +//│ define apply⁰ as fun apply¹(value: Int⁰, f: Function⁰): Int⁰ { +//│ return (f(value) as! Int⁰) +//│ }; +//│ define lambda as fun lambda⁰(_0: Int⁰): ? { return +⁰(_0, 1) }; +//│ return apply¹(41, lambda⁰) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 42 + + +// Overrides are checked against the representations of the members they override, on every target. +:ge +private class Base() with + open fun get() = new Base() +private class Derived() extends Base() with + override fun get() = "str" +private fun produce(b) = b.get() +produce(Derived()) +//│ ╔══[COMPILATION ERROR] Overriding member 'get' produces values represented as 'Str', which the member it overrides cannot produce: its results are represented as 'Base' +//│ ║ l.231: override fun get() = "str" +//│ ║ ^^^ +//│ ╟── The overridden member 'get' is defined here +//│ ║ l.229: open fun get() = new Base() +//│ ╙── ^^^ +//│ = "str" diff --git a/hkmc2/shared/src/test/mlscript/newres/Functions.mls b/hkmc2/shared/src/test/mlscript/newres/Functions.mls index 2c04237801..81cfef476f 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Functions.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Functions.mls @@ -147,22 +147,6 @@ callid1(Predef) //│ | | listen: trm = [1] //│ | | listenTerm: trm = 1 //│ | | listen: trm = 1 -//│ | | listen: trm = member:Num⁰ -//│ | | transportType type of member:Num⁰ (previous = List(), marks = List(↘⟨class:Int⁰⟩ϵ)) -//│ | | | .exit InstanceShape(missing) (↘⟨class:Int⁰⟩ϵ) -//│ | | | | .exit InstanceShape(missing) (ϵ) -//│ | | | | = InstanceShape(missing) -//│ | | | = MarkedShape(InstanceShape(missing), ↘⟨class:Int⁰⟩ϵ) -//│ | | .enter InstanceShape(missing) (↘⟨class:Int⁰⟩ϵ) -//│ | | | .exit MarkedShape (InstanceShape(missing), class:Int⁰, ) -//│ | | | = MarkedShape(InstanceShape(missing), ↗⟨class:Int⁰⟩ϵ) -//│ | | | .enter MarkedShape(InstanceShape(missing), ↗⟨class:Int⁰⟩ϵ) (ϵ) -//│ | | | = MarkedShape(InstanceShape(missing), ↗⟨class:Int⁰⟩ϵ) -//│ | | = MarkedShape(InstanceShape(missing), ↗⟨class:Int⁰⟩ϵ) -//│ | | .exit NominalInstanceView(class:Num¹) (↘⟨class:Int⁰⟩ϵ) -//│ | | | .exit NominalInstanceView(class:Num¹) (ϵ) -//│ | | | = NominalInstanceView(class:Num¹) -//│ | | = MarkedShape(NominalInstanceView(class:Num¹), ↘⟨class:Int⁰⟩ϵ) //│ | | introShape: intro = Lit(IntLit(1)) //│ | | .exit IntroShape(1) (ϵ) //│ | | = IntroShape(1) @@ -208,6 +192,10 @@ callid1(Predef) //│ | | = IntroShape(1) //│ | ~> member:callid1⁰(member:Predef⁰) //│ ~> { fun member:callid1⁰(p⁰) = p⁰.id‹term:Predef¹/id¹›(1); member:callid1⁰(member:Predef⁰) } +//│ listenTerm: trm = p⁰ +//│ listen: trm = p⁰ +//│ listenTerm: trm = p⁰.id‹term:Predef¹/id¹›(1) +//│ listen: trm = p⁰.id‹term:Predef¹/id¹›(1) //│ listenTerm: trm = member:callid1⁰(member:Predef⁰) //│ listen: trm = member:callid1⁰(member:Predef⁰) //│ ————————————————| Resolved |———————————————————————————————————————————————————————————————————————— @@ -270,27 +258,27 @@ f.name :e (x => x).foo //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.271: (x => x).foo +//│ ║ l.259: (x => x).foo //│ ║ ^^^^^^^^^^^^ //│ ╟── Function literal does not contain member 'foo' -//│ ║ l.271: (x => x).foo +//│ ║ l.259: (x => x).foo //│ ╙── ^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. :e f is Int -> Int //│ ╔══[COMPILATION ERROR] Unrecognized pattern (infix operator '->'). -//│ ║ l.281: f is Int -> Int +//│ ║ l.269: f is Int -> Int //│ ╙── ^^^^^^^^^^ //│ = true :fixme (f as Int -> Int).apply //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.288: (f as Int -> Int).apply +//│ ║ l.276: (f as Int -> Int).apply //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Function with a declared signature does not contain member 'apply' -//│ ║ l.288: (f as Int -> Int).apply +//│ ║ l.276: (f as Int -> Int).apply //│ ╙── ^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. diff --git a/hkmc2/shared/src/test/mlscript/newres/GeneratorResults.mls b/hkmc2/shared/src/test/mlscript/newres/GeneratorResults.mls index 10020889ba..a4c01b5660 100644 --- a/hkmc2/shared/src/test/mlscript/newres/GeneratorResults.mls +++ b/hkmc2/shared/src/test/mlscript/newres/GeneratorResults.mls @@ -1,18 +1,32 @@ :. // A generator call returns an iterator even when its body returns a literal. -// Resolution currently propagates that literal instead of the iterator shape. @annotations.generator fun generated() = 1 -:fixme :expect {value: 1, done: true} generated().next() -//│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.10: generated().next() -//│ ║ ^^^^^^^^^^^^^^^^ -//│ ╟── Integer literal does not contain member 'next' -//│ ║ l.6: fun generated() = 1 -//│ ╙── ^ -//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. -//│ ═══[RUNTIME ERROR] Expected: '{value: 1, done: true}', got: 'undefined' +//│ = {value: 1, done: true} + + +// The callable's representation is the iterator; the body's result is only the payload +// of the final step. Bindings of the call therefore hold an iterator, not an integer. +:sir +:siret +@annotations.generator +fun counted() = + yield 0 + 1 +let iterator = counted() +iterator.next() +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ @generator +//│ define counted⁰ as fun counted¹(): Iterator⁰ { +//│ do yield⁰(0); +//│ return 1 +//│ }; +//│ set iterator⁰ = counted¹(); +//│ return iterator⁰.next⁰() +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = {value: 0, done: false} +//│ iterator = {} diff --git a/hkmc2/shared/src/test/mlscript/newres/InvalidClassCompanions.mls b/hkmc2/shared/src/test/mlscript/newres/InvalidClassCompanions.mls index 3d4e694b91..9a893b6ec1 100644 --- a/hkmc2/shared/src/test/mlscript/newres/InvalidClassCompanions.mls +++ b/hkmc2/shared/src/test/mlscript/newres/InvalidClassCompanions.mls @@ -36,11 +36,11 @@ fun Getter = 14 :e module MutableMembers { class Mutable - mut val Mutable = 17 + mut val Mutable: Int = 17 } //│ ╔══[COMPILATION ERROR] A class companion must be a function with a parameter list -//│ ║ l.39: mut val Mutable = 17 -//│ ║ ^^^^^^^^^^^^^^^^ +//│ ║ l.39: mut val Mutable: Int = 17 +//│ ║ ^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Arbitrary values cannot be overloaded with class 'Mutable' //│ ║ l.38: class Mutable //│ ╙── ^^^^^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/newres/Opens.mls b/hkmc2/shared/src/test/mlscript/newres/Opens.mls index 18b48d1bd9..d247a1c0cb 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Opens.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Opens.mls @@ -141,7 +141,7 @@ open Later { absent } module Overloaded { class Item { fun answer() = 9 } fun Item() = 11 - mut val value = 11 + mut val value: Int = 11 } fun overload() = { open Overloaded diff --git a/hkmc2/shared/src/test/mlscript/newres/OverloadedClassValues.mls b/hkmc2/shared/src/test/mlscript/newres/OverloadedClassValues.mls index 6e2d37698b..043a3b8b1d 100644 --- a/hkmc2/shared/src/test/mlscript/newres/OverloadedClassValues.mls +++ b/hkmc2/shared/src/test/mlscript/newres/OverloadedClassValues.mls @@ -155,7 +155,7 @@ module MutableMembers { class Item { fun answer() = 53 } - mut val value = 17 + mut val value: Int = 17 } set MutableMembers.value = 18 diff --git a/hkmc2/shared/src/test/mlscript/newres/Overrides.mls b/hkmc2/shared/src/test/mlscript/newres/Overrides.mls index 340310a7f7..dd96b2cdb9 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Overrides.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Overrides.mls @@ -78,7 +78,9 @@ new Abstract :sir :expect 10 -class SuperBase { open fun foo(x) = x + 1 } +// Open members of public classes are exposed to other compilation units, which can override them, +// so their parameter and result types are annotated. +class SuperBase { open fun foo(x: Int): Int = x + 1 } class SuperChild extends SuperBase { override fun foo(x) = super.foo(x + 1) * 2 } new SuperChild().foo(3) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— @@ -145,7 +147,7 @@ class GenericChild[A](x: A) extends GenericBase[A](x) { :fixme :expect 8 class ForwardChild extends ForwardBase { override fun f(x) = super.f(x) + 1 } -class ForwardBase { open fun f(x) = x + 2 } +class ForwardBase { open fun f(x: Int): Int = x + 2 } new ForwardChild().f(5) //│ ═══[RUNTIME ERROR] TypeError: Class extends value undefined is not a constructor or null //│ ═══[RUNTIME ERROR] Expected: '8', got: 'undefined' @@ -153,23 +155,23 @@ new ForwardChild().f(5) :e super.foo() //│ ╔══[COMPILATION ERROR] Cannot use 'super' outside of an object scope -//│ ║ l.154: super.foo() +//│ ║ l.156: super.foo() //│ ╙── ^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. :e class NoParent { fun f() = super.missing() } //│ ╔══[COMPILATION ERROR] Resolution error in super reference -//│ ║ l.161: class NoParent { fun f() = super.missing() } +//│ ║ l.163: class NoParent { fun f() = super.missing() } //│ ║ ^^^^^^^^^^^^^ //│ ╟── No inherited member 'missing' is available through 'super' -//│ ║ l.161: class NoParent { fun f() = super.missing() } +//│ ║ l.163: class NoParent { fun f() = super.missing() } //│ ╙── ^^^^^^^^^^^^^ :e class AbstractSuper extends Abstract { fun foo() = super.foo() } //│ ╔══[COMPILATION ERROR] Super reference 'foo' requires one inherited implementation -//│ ║ l.170: class AbstractSuper extends Abstract { fun foo() = super.foo() } +//│ ║ l.172: class AbstractSuper extends Abstract { fun foo() = super.foo() } //│ ║ ^^^^^^^^^ //│ ╟── Candidate 'foo' is declared here //│ ║ l.53: abstract class Abstract { fun foo() } @@ -179,25 +181,25 @@ class AbstractSuper extends Abstract { fun foo() = super.foo() } class PrivateBase { private fun secret() = 1 } class PrivateSuper extends PrivateBase { fun f() = super.secret() } //│ ╔══[COMPILATION ERROR] Resolution error in super reference -//│ ║ l.180: class PrivateSuper extends PrivateBase { fun f() = super.secret() } +//│ ║ l.182: class PrivateSuper extends PrivateBase { fun f() = super.secret() } //│ ║ ^^^^^^^^^^^^ //│ ╟── No inherited member 'secret' is available through 'super' -//│ ║ l.180: class PrivateSuper extends PrivateBase { fun f() = super.secret() } +//│ ║ l.182: class PrivateSuper extends PrivateBase { fun f() = super.secret() } //│ ╙── ^^^^^^^^^^^^ :e class ConcreteMissing { fun foo() } //│ ╔══[COMPILATION ERROR] Class 'ConcreteMissing' has unimplemented members -//│ ║ l.189: class ConcreteMissing { fun foo() } +//│ ║ l.191: class ConcreteMissing { fun foo() } //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Member 'foo' requires an implementation -//│ ║ l.189: class ConcreteMissing { fun foo() } +//│ ║ l.191: class ConcreteMissing { fun foo() } //│ ╙── ^^^ :e object MissingObject extends Abstract //│ ╔══[COMPILATION ERROR] Object 'MissingObject' has unimplemented members -//│ ║ l.198: object MissingObject extends Abstract +//│ ║ l.200: object MissingObject extends Abstract //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Member 'foo' requires an implementation //│ ║ l.53: abstract class Abstract { fun foo() } @@ -206,7 +208,7 @@ object MissingObject extends Abstract :e class BadKind extends Base { override fun foo = 1 } //│ ╔══[COMPILATION ERROR] Overriding member 'foo' must preserve whether the member is a field, getter, or method -//│ ║ l.207: class BadKind extends Base { override fun foo = 1 } +//│ ║ l.209: class BadKind extends Base { override fun foo = 1 } //│ ║ ^^^^^^^^^^^^^^^^^^^^ //│ ╟── Inherited member 'foo' is defined here //│ ║ l.3: class Base { open fun foo() = 1 } @@ -215,20 +217,20 @@ class BadKind extends Base { override fun foo = 1 } :e class AbstractCall() { fun f() } //│ ╔══[COMPILATION ERROR] Class 'AbstractCall' has unimplemented members -//│ ║ l.216: class AbstractCall() { fun f() } +//│ ║ l.218: class AbstractCall() { fun f() } //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Member 'f' requires an implementation -//│ ║ l.216: class AbstractCall() { fun f() } +//│ ║ l.218: class AbstractCall() { fun f() } //│ ╙── ^ :e abstract class AbstractCallable() { fun f() } AbstractCallable() //│ ╔══[COMPILATION ERROR] Cannot instantiate abstract class 'AbstractCallable' -//│ ║ l.226: AbstractCallable() +//│ ║ l.228: AbstractCallable() //│ ║ ^^^^^^^^^^^^^^^^^^ //│ ╟── Abstract class defined here -//│ ║ l.225: abstract class AbstractCallable() { fun f() } +//│ ║ l.227: abstract class AbstractCallable() { fun f() } //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -245,7 +247,7 @@ AbstractCallable() :e new Abstract with { fun other() = 9 } //│ ╔══[COMPILATION ERROR] Class '$anon' has unimplemented members -//│ ║ l.246: new Abstract with { fun other() = 9 } +//│ ║ l.248: new Abstract with { fun other() = 9 } //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Member 'foo' requires an implementation //│ ║ l.53: abstract class Abstract { fun foo() } @@ -255,10 +257,10 @@ new Abstract with { fun other() = 9 } :e class PrivateAbstract { private fun hidden() } //│ ╔══[COMPILATION ERROR] Class 'PrivateAbstract' has unimplemented members -//│ ║ l.256: class PrivateAbstract { private fun hidden() } +//│ ║ l.258: class PrivateAbstract { private fun hidden() } //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Member 'hidden' requires an implementation -//│ ║ l.256: class PrivateAbstract { private fun hidden() } +//│ ║ l.258: class PrivateAbstract { private fun hidden() } //│ ╙── ^^^^^^ // The nearest common ancestor is used even when a deeper ancestor is also common. @@ -287,10 +289,10 @@ class Unrelated { fun foo() = 0 } private fun ambiguous(x) = x.foo() [ambiguous(new Base), ambiguous(new Unrelated)] //│ ╔══[COMPILATION ERROR] This selection of member 'foo' is ambiguous, as it has multiple resolved targets -//│ ║ l.287: private fun ambiguous(x) = x.foo() +//│ ║ l.289: private fun ambiguous(x) = x.foo() //│ ║ ^^^^^ //│ ╟── target: function 'foo' in class 'Unrelated' -//│ ║ l.286: class Unrelated { fun foo() = 0 } +//│ ║ l.288: class Unrelated { fun foo() = 0 } //│ ║ ^^^ //│ ╟── target: function 'foo' in class 'Base' //│ ║ l.3: class Base { open fun foo() = 1 } @@ -300,7 +302,7 @@ private fun ambiguous(x) = x.foo() :e class ClosedAgain extends Left { override fun foo() = 4 } //│ ╔══[COMPILATION ERROR] Cannot override non-open member 'foo' -//│ ║ l.301: class ClosedAgain extends Left { override fun foo() = 4 } +//│ ║ l.303: class ClosedAgain extends Left { override fun foo() = 4 } //│ ║ ^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Inherited member 'foo' is defined here //│ ║ l.4: class Left extends Base { override fun foo() = 2 } @@ -310,7 +312,7 @@ class ClosedAgain extends Left { override fun foo() = 4 } abstract class MissingInherited extends Abstract class StillMissing extends MissingInherited //│ ╔══[COMPILATION ERROR] Class 'StillMissing' has unimplemented members -//│ ║ l.311: class StillMissing extends MissingInherited +//│ ║ l.313: class StillMissing extends MissingInherited //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Member 'foo' requires an implementation //│ ║ l.53: abstract class Abstract { fun foo() } @@ -342,7 +344,7 @@ new StoredParent().f() let abstractValue = Abstract new abstractValue //│ ╔══[COMPILATION ERROR] Cannot instantiate abstract class 'Abstract' -//│ ║ l.343: new abstractValue +//│ ║ l.345: new abstractValue //│ ║ ^^^^^^^^^^^^^^^^^ //│ ╟── Abstract class defined here //│ ║ l.53: abstract class Abstract { fun foo() } @@ -354,10 +356,10 @@ new abstractValue let abstractCtor = AbstractCallable abstractCtor() //│ ╔══[COMPILATION ERROR] Cannot instantiate abstract class 'AbstractCallable' -//│ ║ l.355: abstractCtor() +//│ ║ l.357: abstractCtor() //│ ║ ^^^^^^^^^^^^^^ //│ ╟── Abstract class defined here -//│ ║ l.225: abstract class AbstractCallable() { fun f() } +//│ ║ l.227: abstract class AbstractCallable() { fun f() } //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ abstractCtor = fun AbstractCallable { class: class AbstractCallable } @@ -366,7 +368,7 @@ abstractCtor() let abstractParent = Abstract class StoredIncomplete extends abstractParent //│ ╔══[COMPILATION ERROR] Class 'StoredIncomplete' has unimplemented members -//│ ║ l.367: class StoredIncomplete extends abstractParent +//│ ║ l.369: class StoredIncomplete extends abstractParent //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Member 'foo' requires an implementation //│ ║ l.53: abstract class Abstract { fun foo() } @@ -409,10 +411,10 @@ abstract class PrivateRequirement { private fun hidden() } :e class InheritedPrivateRequirement extends PrivateRequirement { fun hidden() = 1 } //│ ╔══[COMPILATION ERROR] Class 'InheritedPrivateRequirement' has unimplemented members -//│ ║ l.410: class InheritedPrivateRequirement extends PrivateRequirement { fun hidden() = 1 } +//│ ║ l.412: class InheritedPrivateRequirement extends PrivateRequirement { fun hidden() = 1 } //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Member 'hidden' requires an implementation -//│ ║ l.407: abstract class PrivateRequirement { private fun hidden() } +//│ ║ l.409: abstract class PrivateRequirement { private fun hidden() } //│ ╙── ^^^^^^ // Different receiver classes can inherit the same declaration without overriding it. @@ -434,7 +436,7 @@ let changingParent = makeParent(4) class SavedParent extends changingParent { override fun f() = super.f() } set changingParent = makeParent(9) new SavedParent().f() -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'NewResolver.scala:3689' +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'NewResolver.scala:3881' //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. //│ = 4 @@ -449,7 +451,7 @@ let changingHolder = Holder(5) class SavedNestedParent extends changingHolder.Parent { override fun f() = super.f() } set changingHolder = Holder(11) new SavedNestedParent().f() -//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'NewResolver.scala:3689' +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'NewResolver.scala:3881' //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. //│ = 5 diff --git a/hkmc2/shared/src/test/mlscript/newres/ParamClasses.mls b/hkmc2/shared/src/test/mlscript/newres/ParamClasses.mls index de1449c5e7..7f282bf392 100644 --- a/hkmc2/shared/src/test/mlscript/newres/ParamClasses.mls +++ b/hkmc2/shared/src/test/mlscript/newres/ParamClasses.mls @@ -132,7 +132,9 @@ val fv = Foo(1) //│ fun get⁰::get¹ ‹flow:get⁰› = fv¹^get¹.a⁴, val fv¹::fv² ‹flow:fv⁰› = Foo⁴^fv²(1)‹app⁵› //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let tmp; -//│ define get⁰ as fun get¹() { return fv⁰.a⁰ }; +//│ define get⁰ as fun get¹() { +//│ return (fv⁰.a⁰ as! Int⁰) +//│ }; //│ set tmp = Foo⁰(1); //│ define fv¹ as val fv⁰ = tmp; //│ end @@ -229,17 +231,17 @@ let g = f(1) :e g(2) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.230: g(2) +//│ ║ l.232: g(2) //│ ║ ^^^^ //│ ╟── Instance of class 'Foo' cannot receive more argument lists. -//│ ║ l.226: let g = f(1) +//│ ║ l.228: let g = f(1) //│ ╙── ^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. :e let f = new Foo() //│ ╔══[COMPILATION ERROR] Resolution error in object instantiation -//│ ║ l.240: let f = new Foo() +//│ ║ l.242: let f = new Foo() //│ ║ ^^^^^^^^^ //│ ╟── Constructor of class 'Foo' expected 1 argument, but got 0 //│ ║ l.80: class Foo(val a) { @@ -250,10 +252,10 @@ let f = new Foo() :e new 42 //│ ╔══[COMPILATION ERROR] Resolution error in object instantiation -//│ ║ l.251: new 42 +//│ ║ l.253: new 42 //│ ║ ^^^^^^ //│ ╟── Invalid class expression: integer literal -//│ ║ l.251: new 42 +//│ ║ l.253: new 42 //│ ╙── ^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. diff --git a/hkmc2/shared/src/test/mlscript/newres/PartialSignatures.mls b/hkmc2/shared/src/test/mlscript/newres/PartialSignatures.mls index 1dc9ea019f..6bcc7d14be 100644 --- a/hkmc2/shared/src/test/mlscript/newres/PartialSignatures.mls +++ b/hkmc2/shared/src/test/mlscript/newres/PartialSignatures.mls @@ -131,6 +131,12 @@ read(new Child) //│ ╟── The shape of this value is unknown. //│ ║ l.125: private fun read(x: Base) = x.item().value //│ ╙── ^^^^^^ +//│ ╔══[COMPILATION ERROR] Overriding member 'item' produces values represented as 'Other', which the member it overrides cannot produce: its results are represented as 'Item' +//│ ║ l.124: override fun item() = Other(2) +//│ ║ ^^^^ +//│ ╟── The overridden member 'item' is defined here +//│ ║ l.122: open fun item() = Item(1) +//│ ╙── ^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. // A separate polymorphic signature retains binders for explicit type application. @@ -158,10 +164,10 @@ class Child extends Base with let identity = (x => x) as [A] -> A -> A identity[Base](new Child).extra //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.159: identity[Base](new Child).extra +//│ ║ l.165: identity[Base](new Child).extra //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Value of type 'Base' does not contain member 'extra' -//│ ║ l.159: identity[Base](new Child).extra +//│ ║ l.165: identity[Base](new Child).extra //│ ╙── ^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ identity = fun identity @@ -171,10 +177,10 @@ identity[Base](new Child).extra let identity = (x => x) as [A] -> A -> A identity[Int, Int](1) //│ ╔══[COMPILATION ERROR] Resolution error in type application -//│ ║ l.172: identity[Int, Int](1) +//│ ║ l.178: identity[Int, Int](1) //│ ║ ^^^^^^^^^^^^^^^^^^ //│ ╟── Function with a declared signature expected 1 type argument, but got 2 -//│ ║ l.171: let identity = (x => x) as [A] -> A -> A +//│ ║ l.177: let identity = (x => x) as [A] -> A -> A //│ ╙── ^^^^^^ //│ = 1 //│ identity = fun identity diff --git a/hkmc2/shared/src/test/mlscript/newres/PatMat.mls b/hkmc2/shared/src/test/mlscript/newres/PatMat.mls index 872556b040..e318e2c74f 100644 --- a/hkmc2/shared/src/test/mlscript/newres/PatMat.mls +++ b/hkmc2/shared/src/test/mlscript/newres/PatMat.mls @@ -113,10 +113,19 @@ test(bz) //│ ═══[RUNTIME ERROR] Error: match error +// The earlier worksheet block fixed x's representation to Foo. +:ge set x = bz +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Baz' at an unrelated type 'Foo' +//│ ║ l.118: set x = bz +//│ ╙── ^^^^^^ :breakme // Note: if compiled together with it, this would make the previous code ambiguous +:ge x = bz +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Baz' at an unrelated type 'Foo' +//│ ║ l.24: let x = Foo(Bar) +//│ ╙── ^ let x = if maybe then Bar else bz @@ -125,7 +134,7 @@ let x = if maybe then Bar else bz :e x.b //│ ╔══[COMPILATION ERROR] This selection of member 'b' is ambiguous, as it has multiple resolved targets -//│ ║ l.126: x.b +//│ ║ l.135: x.b //│ ║ ^^^ //│ ╟── target: function 'b' in overloaded function and class 'Baz' //│ ║ l.7: class Baz(c) { fun b = c } @@ -167,7 +176,7 @@ if Foo(Predef) is val f = case Foo(y) then y.b //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.168: Foo(y) then y.b +//│ ║ l.177: Foo(y) then y.b //│ ║ ^^^ //│ ╟── Cannot resolve member 'b' of a value with unknown shape. //│ ╟── This constructor field may contain a value of unknown shape. @@ -175,7 +184,7 @@ val f = case //│ ║ ^ //│ ╟── Parameter 'caseScrut' admits values of unknown shape. //│ ╟── 'f' is exposed by this compilation unit. -//│ ║ l.167: val f = case +//│ ║ l.176: val f = case //│ ╙── ^^^^^ //│ f = fun diff --git a/hkmc2/shared/src/test/mlscript/newres/Projections.mls b/hkmc2/shared/src/test/mlscript/newres/Projections.mls index 69273e58a0..d127325072 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Projections.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Projections.mls @@ -37,7 +37,7 @@ new Child().Child#get() // Fields and assignments share the same resolution path. :expect 12 -class Cell { mut val value = 10 } +class Cell { mut val value: Int = 10 } let cell = new Cell set cell.Cell#value = 12 cell.Cell#value @@ -105,7 +105,7 @@ apply(identity, new Answer).get() // The receiver of a projection is evaluated once, including writes. :expect 1 -module Counter { mut val count = 0 } +module Counter { mut val count: Int = 0 } fun receiver() = { set Counter.count = Counter.count + 1 cell diff --git a/hkmc2/shared/src/test/mlscript/newres/Records.mls b/hkmc2/shared/src/test/mlscript/newres/Records.mls index c4465ba1d0..a3f5bcc300 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Records.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Records.mls @@ -144,19 +144,24 @@ mutableRecord.answer //│ = 20 //│ mutableRecord = {answer: 20} -// TODO: Track writes to mutable fields. Their initializer must not determine -// static targets after an assignment (even if another receiver has that member). +// Reading a mutable field returns the values assigned to it, but also its initializer, as resolution is not +// flow-sensitive. Since `changing` is exposed, other compilation units can also assign values of unknown shape. +// TODO: The initializer must not determine static targets after an assignment +// (even if another receiver has that member). :fixme let changing = mut {value: {before: 0}} set changing.value = {after: 21} changing.value.after //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.152: changing.value.after +//│ ║ l.154: changing.value.after //│ ║ ^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Record literal does not contain member 'after' +//│ ║ l.152: let changing = mut {value: {before: 0}} +//│ ║ ^^^^^^^^^ //│ ╟── Cannot resolve member 'after' of a value with unknown shape. -//│ ╟── The shape of this value is unknown. -//│ ║ l.150: let changing = mut {value: {before: 0}} -//│ ╙── ^^^^^ +//│ ╟── This value is exposed by this compilation unit. +//│ ║ l.152: let changing = mut {value: {before: 0}} +//│ ╙── ^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ changing = {value: {after: 21}} @@ -168,30 +173,30 @@ fun nestedWrap(x) = {...wrap(x)} nestedWrap({first: 22}).value.first nestedWrap({second: 23}).value.second //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.168: nestedWrap({first: 22}).value.first +//│ ║ l.173: nestedWrap({first: 22}).value.first //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Record literal does not contain member 'first' -//│ ║ l.169: nestedWrap({second: 23}).value.second +//│ ║ l.174: nestedWrap({second: 23}).value.second //│ ║ ^^^^^^^^^^ //│ ╟── Cannot resolve member 'first' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.167: fun nestedWrap(x) = {...wrap(x)} +//│ ║ l.172: fun nestedWrap(x) = {...wrap(x)} //│ ║ ^ //│ ╟── 'nestedWrap' is exposed by this compilation unit. -//│ ║ l.167: fun nestedWrap(x) = {...wrap(x)} +//│ ║ l.172: fun nestedWrap(x) = {...wrap(x)} //│ ╙── ^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.169: nestedWrap({second: 23}).value.second +//│ ║ l.174: nestedWrap({second: 23}).value.second //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Record literal does not contain member 'second' -//│ ║ l.168: nestedWrap({first: 22}).value.first +//│ ║ l.173: nestedWrap({first: 22}).value.first //│ ║ ^^^^^^^^^ //│ ╟── Cannot resolve member 'second' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.167: fun nestedWrap(x) = {...wrap(x)} +//│ ║ l.172: fun nestedWrap(x) = {...wrap(x)} //│ ║ ^ //│ ╟── 'nestedWrap' is exposed by this compilation unit. -//│ ║ l.167: fun nestedWrap(x) = {...wrap(x)} +//│ ║ l.172: fun nestedWrap(x) = {...wrap(x)} //│ ╙── ^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -207,11 +212,11 @@ fun unknownOpen() = a unknownOpen() //│ ╔══[COMPILATION ERROR] Resolution error in wildcard-open reference -//│ ║ l.207: a +//│ ║ l.212: a //│ ║ ^ //│ ╟── Cannot resolve member 'a' across a computed key or unknown record spread. //│ ╟── This computed key can overwrite the selected member. -//│ ║ l.202: let unknownKeys = {(key): 0} +//│ ║ l.207: let unknownKeys = {(key): 0} //│ ╙── ^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ unknownKeys = {a: 0} @@ -234,13 +239,13 @@ forwardRecord.answer fun choose(flag) = if flag then {answer: 1} else {answer: 2} choose(true).answer //│ ╔══[COMPILATION ERROR] This selection of member 'answer' is ambiguous, as it has multiple resolved targets -//│ ║ l.235: choose(true).answer +//│ ║ l.240: choose(true).answer //│ ║ ^^^^^^^^^^^^^^^^^^^ //│ ╟── target: record field 'answer' -//│ ║ l.234: fun choose(flag) = if flag then {answer: 1} else {answer: 2} +//│ ║ l.239: fun choose(flag) = if flag then {answer: 1} else {answer: 2} //│ ║ ^^^^^^ //│ ╟── target: record field 'answer' -//│ ║ l.234: fun choose(flag) = if flag then {answer: 1} else {answer: 2} +//│ ║ l.239: fun choose(flag) = if flag then {answer: 1} else {answer: 2} //│ ╙── ^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -252,32 +257,36 @@ fun recursiveRecord(n) = else {...recursiveRecord(n - 1)} recursiveRecord(2).answer //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.253: recursiveRecord(2).answer +//│ ║ l.258: recursiveRecord(2).answer //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Cannot resolve member 'answer' across a computed key or unknown record spread. //│ ╟── This spread has no known element shape. -//│ ║ l.252: else {...recursiveRecord(n - 1)} +//│ ║ l.257: else {...recursiveRecord(n - 1)} //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. // Unknown mutable values in an open must not disappear in favor of another source. -:fixme -fun unknownValueOpen() = +:e +fun unknownValueOpen(input) = let unknownValue = mut {value: {answer3: 0}} + set unknownValue.value = input let fallbackValue = {answer3: 1} let openedValue = unknownValue.value open openedValue open fallbackValue answer3 -unknownValueOpen() +unknownValueOpen({answer3: 2}) //│ ╔══[COMPILATION ERROR] Resolution error in wildcard-open reference -//│ ║ l.272: answer3 +//│ ║ l.278: answer3 //│ ║ ^^^^^^^ //│ ╟── Cannot resolve member 'answer3' of a value with unknown shape. -//│ ╟── The shape of this value is unknown. -//│ ║ l.267: let unknownValue = mut {value: {answer3: 0}} -//│ ╙── ^^^^^ +//│ ╟── Parameter 'input' admits values of unknown shape. +//│ ║ l.271: fun unknownValueOpen(input) = +//│ ║ ^^^^^ +//│ ╟── 'unknownValueOpen' is exposed by this compilation unit. +//│ ║ l.271: fun unknownValueOpen(input) = +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -300,7 +309,7 @@ fun allMissingOpen() = nonexistent allMissingOpen() //│ ╔══[COMPILATION ERROR] Name not found in wildcard opens: nonexistent -//│ ║ l.300: nonexistent +//│ ║ l.309: nonexistent //│ ╙── ^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -337,21 +346,24 @@ set mutableCopy.value = 32 //│ mutableCopy = {value: 32} //│ stableRecord = {value: 31} -// TODO: Infer the new value of mutableNested.value after the assignment. Until then, -// report its unknown value shape rather than assuming it still contains `before`. -// The unchanged stableNested.value.before must remain resolvable. +// The unchanged stableNested.value.before must remain resolvable. Reading mutableNested.value returns the value +// assigned to it, but also the initializer of the field it copies, as resolution is not flow-sensitive. +// TODO: Do not assume that mutableNested.value still contains `before` after the assignment. :fixme let stableNested = {value: {before: 33}} let mutableNested = mut {...stableNested} set mutableNested.value = {after: 34} [stableNested.value.before, mutableNested.value.after] //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.347: [stableNested.value.before, mutableNested.value.after] +//│ ║ l.356: [stableNested.value.before, mutableNested.value.after] //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Record literal does not contain member 'after' +//│ ║ l.353: let stableNested = {value: {before: 33}} +//│ ║ ^^^^^^^^^^ //│ ╟── Cannot resolve member 'after' of a value with unknown shape. -//│ ╟── The shape of this value is unknown. -//│ ║ l.344: let stableNested = {value: {before: 33}} -//│ ╙── ^^^^^ +//│ ╟── This value is exposed by this compilation unit. +//│ ║ l.354: let mutableNested = mut {...stableNested} +//│ ╙── ^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ mutableNested = {value: {after: 34}} //│ stableNested = {value: {before: 33}} @@ -367,18 +379,18 @@ fun ambiguousRecordOpen() = shared ambiguousRecordOpen() //│ ╔══[COMPILATION ERROR] Wildcard-open reference 'shared' is ambiguous -//│ ║ l.367: shared +//│ ║ l.379: shared //│ ║ ^^^^^^ //│ ╟── candidate: record field 'shared' defined here -//│ ║ l.363: let firstRecord = {shared: 1} +//│ ║ l.375: let firstRecord = {shared: 1} //│ ║ ^^^^^^ //│ ╟── Opened here -//│ ║ l.365: open firstRecord +//│ ║ l.377: open firstRecord //│ ║ ^^^^^^^^^^^ //│ ╟── candidate: record field 'shared' defined here -//│ ║ l.364: let secondRecord = {shared: 2} +//│ ║ l.376: let secondRecord = {shared: 2} //│ ║ ^^^^^^ //│ ╟── Opened here -//│ ║ l.366: open secondRecord +//│ ║ l.378: open secondRecord //│ ╙── ^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. diff --git a/hkmc2/shared/src/test/mlscript/newres/ShapeFixture.mls b/hkmc2/shared/src/test/mlscript/newres/ShapeFixture.mls index d1165b8780..6d3f8e16dd 100644 --- a/hkmc2/shared/src/test/mlscript/newres/ShapeFixture.mls +++ b/hkmc2/shared/src/test/mlscript/newres/ShapeFixture.mls @@ -6,11 +6,11 @@ import "../../mlscript-compile/Shape.mls" import "../../mlscript-compile/Option.mls" class Pair(val left: Int, val right: Str) -val pair = Block.ConcreteClassSymbol("Pair", Pair, Option.Some([ +val pair = Block.ConcreteClassSymbol("Pair", Pair.class, Option.Some([ Block.Symbol("left"), Block.Symbol("right")]), []) //│ pair = ConcreteClassSymbol( //│ _, -//│ fun Pair { class: class Pair }, +//│ class Pair, //│ Some([Symbol("left"), Symbol("right")]), //│ [] //│ ) @@ -30,7 +30,7 @@ Shape.sel(pairShape, Shape.Lit("left")).map((s, ...) => Shape.show(s)) //│ pairShape = Class( //│ ConcreteClassSymbol( //│ _, -//│ fun Pair { class: class Pair }, +//│ class Pair, //│ Some([Symbol("left"), Symbol("right")]), //│ [] //│ ), diff --git a/hkmc2/shared/src/test/mlscript/newres/loose/Overrides.mls b/hkmc2/shared/src/test/mlscript/newres/loose/Overrides.mls index 876fddeb2a..ead68491c4 100644 --- a/hkmc2/shared/src/test/mlscript/newres/loose/Overrides.mls +++ b/hkmc2/shared/src/test/mlscript/newres/loose/Overrides.mls @@ -33,7 +33,7 @@ private fun readOther(x) = x.foo() //│ let tmp, tmp1, tmp2, tmp3; //│ @private //│ define readOther⁰ as fun readOther¹(x) { -//│ return x.foo﹖() +//│ return (x.foo﹖() as! Int⁰) //│ }; //│ define Other⁰ as class Other¹ { //│ method foo¹ = fun foo²() { diff --git a/hkmc2/shared/src/test/mlscript/newres/wasm/Erasure.mls b/hkmc2/shared/src/test/mlscript/newres/wasm/Erasure.mls index 7fdea1dddf..6e7d854d25 100644 --- a/hkmc2/shared/src/test/mlscript/newres/wasm/Erasure.mls +++ b/hkmc2/shared/src/test/mlscript/newres/wasm/Erasure.mls @@ -1,12 +1,16 @@ :. +open wasm { i32 } +open i32 { const } +//│ Wasm result: +//│ = {} // Erasure must prepare an unboxed call target before lowering the caller's body. :expect 42 -fun caller(): Number = later(wasm.i32.const(41)) -fun later(value: Number): Number = wasm.i32.add(value, wasm.i32.const(1)) +fun caller(): Number = later(const(41)) +fun later(value: Number): Number = wasm.i32.add(value, const(1)) type Number = LaterNumber type LaterNumber = Int32 caller() @@ -19,6 +23,340 @@ caller() class Box(val value: Number) with fun get(): Number = value type Number = Int32 -Box(wasm.i32.const(42)).get() +Box(const(42)).get() +//│ Wasm result: +//│ = 42 + + +// i32: infer bindings, function inputs/results, and constructor fields together. +:noInline +:expect 42 +private class Slot(val value) +private fun roundtrip(value) = + let saved = value + Slot(saved).value +let input = const(42) +roundtrip(input) +//│ Wasm result: +//│ = 42 + + +// i64: infer bindings, function inputs/results, and constructor fields together. +:noInline +:expect 4294967296 +private class Slot(val value) +private fun roundtrip(value) = + let saved = value + Slot(saved).value +let input = wasm.i64.const(4294967296) +roundtrip(input) +//│ Wasm result: +//│ = 4294967296n + + +// f32: infer bindings, function inputs/results, and constructor fields together. +:noInline +:expect 1.5 +private class Slot(val value) +private fun roundtrip(value) = + let saved = value + Slot(saved).value +let input = wasm.f32.const(1.5) +roundtrip(input) +//│ Wasm result: +//│ = 1.5 + + +// f64: infer bindings, function inputs/results, and constructor fields together. +:noInline +:expect 2.25 +private class Slot(val value) +private fun roundtrip(value) = + let saved = value + Slot(saved).value +let input = wasm.f64.const(2.25) +roundtrip(input) +//│ Wasm result: +//│ = 2.25 + + +// A forward call and all return paths contribute to the same result slot. +:noInline +:expect 42 +private fun caller() = later(true) +private fun later(flag) = + if flag do return const(42) + const(99) +caller() //│ Wasm result: //│ = 42 + + +// Recursive and independent callers share one physical parameter representation. +:noInline +:expect 42 +private fun repeat(again, value) = + if again then repeat(false, value) else value +let first = repeat(true, const(40)) +let second = repeat(false, const(2)) +wasm.i32.add(first, second) +//│ Wasm result: +//│ = 42 + + +// Assignments inside nested functions contribute to the storage slot. +:noInline +:expect 42 +let value = const(1) +private fun update() = set value = const(42) +update() +value +//│ Wasm result: +//│ = 42 + + +// Auxiliary constructor parameters have the same representation as their generated fields. +:noInline +:expect 42 +private class Box with + constructor(val value) +new Box(const(42)).value +//│ Wasm result: +//│ = 42 + + +// Publish forward parent headers before joining different nominal instances. +:noInline +:expect 42 +private fun choose(flag) = if flag then Left() else Right() +private class Left() extends Parent() +private class Right() extends Parent() +private class Parent() with + val value = const(42) +choose(true).value +//│ Wasm result: +//│ = 42 + + +// A separate full signature supplies an unboxed parameter without a parameter annotation. +:noInline +:expect 42 +fun identity: Int32 -> Int32 +fun identity(value) = value +identity(const(42)) +//│ Wasm result: +//│ = 42 + + +// A getter's inferred result remains an implicitly invoked value. +:noInline +:expect 42 +private class Getter() with + fun value = const(42) +Getter().value +//│ Wasm result: +//│ = 42 + + +// A nonempty join must not ignore an incompatible concrete alternative. +:ge +let value = const(1) +set value = wasm.i64.const(2) +value +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Int64' have no common representation +//│ ║ l.164: set value = wasm.i64.const(2) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Int64' have no common representation +//│ ║ l.163: let value = const(1) +//│ ╙── ^^^^^ +//│ ╔══[COMPILATION ERROR] Cannot cast a Wasm value of type `i32` to `(ref null any)` in a different type hierarchy +//│ ║ l.163: let value = const(1) +//│ ╙── ^^^^^^^^ +//│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `i64` to `(ref null any)` in a different type hierarchy + + +// External callers already contribute Unknown; it conflicts with an unboxed input. +:ge +fun exported(value) = value +exported(const(42)) +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Unknown' have no common representation +//│ ║ l.180: fun exported(value) = value +//│ ╙── ^^^^^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Unknown' have no common representation +//│ ║ l.181: exported(const(42)) +//│ ╙── ^^^^^^^^^ +//│ Wasm result: +//│ = 42 + + +// Explicit Any keeps its declared representation and rejects an unboxed initializer. +:ge +val annotated: Any = const(42) +annotated +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int32' at an unrelated type 'Unknown' +//│ ║ l.194: val annotated: Any = const(42) +//│ ╙── ^^^^^^^^^ +//│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `i32` to `(ref null any)` in a different type hierarchy + + +// Undefined contributes no representation constraint to the result slot. +// Its value is unspecified, so only the concrete branch's result is asserted. +:noInline +:expect 42 +private fun choose(flag, value) = + if flag do return value + unsafe.undefined +let unused = choose(false, const(1)) +choose(true, const(42)) +//│ Wasm result: +//│ = 42 + + +// An explicit undefined value inhabits all four unboxed types and nominal references. +// Exercise parameter, result, field, and local slots without observing the unspecified values. +:noInline +:expect 42 +private class Missing(val a: Int32, val b: Int64, val c: Float32, val d: Float64) +private fun empty(): Missing = unsafe.undefined +private fun pass(a: Int32, b: Int64, c: Float32, d: Float64): Int32 = + val missing = Missing(a, b, c, d) + let local = missing.a + set local = unsafe.undefined + local +let missing = empty() +let unspecified = pass(unsafe.undefined, unsafe.undefined, unsafe.undefined, unsafe.undefined) +42 +//│ Wasm result: +//│ = 42 + + +// Dead-parameter elimination may replace a typed return with undefined. +:noInline +:flowBasedOpt mono +:soir +:expect 42 +private fun discard() = + fun i32(value: Int32) = value + fun i64(value: Int64) = value + fun f32(value: Float32) = value + fun f64(value: Float64) = value + val a = i32(const(1)) + val b = i64(wasm.i64.const(2)) + val c = f32(wasm.f32.const(3.0)) + val d = f64(wasm.f64.const(4.0)) + 42 +discard() +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ @private +//│ define discard⁰ as fun discard¹() { +//│ let f64, f32, i64, i32; +//│ define i32 as fun i32⁰() { +//│ return undefined +//│ }; +//│ define i64 as fun i64⁰() { +//│ return undefined +//│ }; +//│ define f32 as fun f32⁰() { +//│ return undefined +//│ }; +//│ define f64 as fun f64⁰() { +//│ return undefined +//│ }; +//│ do wasm⁰.i32¹.const⁰(1); +//│ do i32⁰(); +//│ do wasm⁰.i64¹.const¹(2); +//│ do i64⁰(); +//│ do wasm⁰.f32¹.const²(3.0); +//│ do f32⁰(); +//│ do wasm⁰.f64¹.const³(4.0); +//│ do f64⁰(); +//│ return 42 +//│ }; +//│ return discard¹() +//│ Wasm result: +//│ = 42 + + +// Returning undefined in an unboxed representation must still execute the call's effects. +:noInline +:expect 42 +let calls = const(0) +private fun absent(): Int64 = + set calls = wasm.i32.add(calls, const(1)) + unsafe.undefined +private fun consume(value: Int64): Int32 = calls +let count = consume(absent()) +wasm.i32.add(count, const(41)) +//│ Wasm result: +//│ = 42 + + +// A branch's undefined value leaves the conditional temporary's representation unconstrained. +:noInline +:expect 42 +private fun choose(flag) = + let value = if flag then const(42) else unsafe.undefined + value +let unused = choose(false) +choose(true) +//│ Wasm result: +//│ = 42 + + +// Exhaustive loop branches repeat or explicitly return; they never return Unit. +:noInline +:expect 42 +private fun loop(flag) = + while flag + then set flag = false + else return const(42) +loop(true) +//│ Wasm result: +//│ = 42 + + +// A returning conditional can make the remainder unreachable, even when it has another representation. +:noInline +:expect 42 +private fun early() = + if true then return const(42) + "unreachable" +early() +//│ Wasm result: +//│ = 42 + + +// A private member does not change the representation of the public member it shadows. +:noInline +:expect 42 +class PublicBase with + fun value = const(42) +class PrivateDerived extends PublicBase with + private fun value = true + fun ownValue = value +let derived = new PrivateDerived +if derived.ownValue then derived.value else const(0) +//│ Wasm result: +//│ = 42 + + +#lang(0.3.x) +//│ Wasm result: +//│ = {} + + +// Trailing arguments retain the inferred unboxed calling convention of the full argument list. +:noInline +:expect 42 +private fun add(left, right) = wasm.i32.add(left, right) +add(const(41)) \ const(1) +//│ Wasm result: +//│ = 42 + + +// A backtracking assignment saves the variable in a temporary, which takes the variable's representation: +// saving and restoring an unboxed `i32` needs no conversion. The Wasm backend does not support `finally` yet. +:ge +let saved = const(1) +set saved = const(2) in saved +//│ ═══[COMPILATION ERROR] WatBuilder::returningTerm for TryBlock(...) not implemented yet diff --git a/hkmc2/shared/src/test/mlscript/newres/wasm/Super.mls b/hkmc2/shared/src/test/mlscript/newres/wasm/Super.mls index 5005cb6608..58651f046e 100644 --- a/hkmc2/shared/src/test/mlscript/newres/wasm/Super.mls +++ b/hkmc2/shared/src/test/mlscript/newres/wasm/Super.mls @@ -1,7 +1,9 @@ :. +// Open members of public classes are exposed to other compilation units, which can override them, +// so their parameter and result types are annotated. :expect 10 -class Base() { open fun f(x) = x + 1 } +class Base() { open fun f(x: Int): Int = x + 1 } class Child() extends Base() { override fun f(x) = super.f(x + 1) * 2 } Child().f(3) //│ Wasm result: @@ -25,7 +27,7 @@ readAbstract(Concrete()) //│ = 7 :expect 10 -class GetterBase() { open fun value = 5 } +class GetterBase() { open fun value: Int = 5 } class GetterChild() extends GetterBase() { override fun value = super.value * 2 } GetterChild().value //│ Wasm result: @@ -86,7 +88,7 @@ FieldChild().x //│ (import "repl" "FieldBase_typeinfo" (global $FieldBase_typeinfo (ref $FieldBase_typeinfo))) //│ (import "repl" "FieldChild_typeinfo" (global $FieldChild_typeinfo (ref $FieldChild_typeinfo))) //│ (func $entry (export "entry") (type $entry) (result (ref null i31)) -//│ (local $tmp (ref null any)) +//│ (local $tmp (ref null $FieldChild)) //│ (block (result (ref null i31)) //│ (local.set $tmp //│ (call $FieldChild_FieldChild)) diff --git a/hkmc2/shared/src/test/mlscript/opt/AbortivePrefix.mls b/hkmc2/shared/src/test/mlscript/opt/AbortivePrefix.mls index f327ea4ecd..4ea573b0b9 100644 --- a/hkmc2/shared/src/test/mlscript/opt/AbortivePrefix.mls +++ b/hkmc2/shared/src/test/mlscript/opt/AbortivePrefix.mls @@ -93,7 +93,7 @@ fun f = //│ end //│ return "2 long long long long long long long long long long long long long long" //│ set tmp1 = tmp; -//│ return Predef⁰.notImplementedError⁰ +//│ return (Predef⁰.notImplementedError⁰ as! Str⁰) //│ }; //│ end //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/opt/AlmostTailCall.mls b/hkmc2/shared/src/test/mlscript/opt/AlmostTailCall.mls index 85b83203b9..301d586f18 100644 --- a/hkmc2/shared/src/test/mlscript/opt/AlmostTailCall.mls +++ b/hkmc2/shared/src/test/mlscript/opt/AlmostTailCall.mls @@ -8,7 +8,7 @@ fun foo() = let x = hide(42) x //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ define foo⁰ as fun foo¹() { return Predef⁰.hide⁰(42) }; end +//│ define foo⁰ as fun foo¹() { return (Predef⁰.hide⁰(42) as! Int⁰) }; end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— // * The call should NOT be moved into the conditional, here, as it is impure @@ -22,7 +22,7 @@ fun foo() = //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define foo² as fun foo³() { //│ let x, scrut; -//│ set x = Predef⁰.hide⁰(42); +//│ set x = (Predef⁰.hide⁰(42) as! Int⁰); //│ set scrut = Predef⁰.maybe⁰; //│ match scrut //│ true => @@ -69,7 +69,7 @@ fun foo() = let y = bar(42) x //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ define foo⁶ as fun foo⁷() { return Predef⁰.hide⁰(42) }; end +//│ define foo⁶ as fun foo⁷() { return (Predef⁰.hide⁰(42) as! Int⁰) }; end //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/opt/BasicVarPropag.mls b/hkmc2/shared/src/test/mlscript/opt/BasicVarPropag.mls index a5a0ce7117..8d50e4be44 100644 --- a/hkmc2/shared/src/test/mlscript/opt/BasicVarPropag.mls +++ b/hkmc2/shared/src/test/mlscript/opt/BasicVarPropag.mls @@ -331,7 +331,7 @@ print(res) //│ match scrut //│ true => //│ let y; -//│ set y = Predef⁰.hide⁰(1); +//│ set y = (Predef⁰.hide⁰(1) as! Int⁰); //│ set res¹ = y; //│ return Predef⁰.print⁰(res¹) //│ else @@ -344,7 +344,7 @@ print(res) //│ match scrut //│ true => //│ let y; -//│ set y = Predef⁰.hide⁰(1); +//│ set y = (Predef⁰.hide⁰(1) as! Int⁰); //│ set res¹ = y; //│ return Predef⁰.print⁰(y) //│ else @@ -369,7 +369,7 @@ hide of print(res) //│ match scrut //│ true => //│ let y; -//│ set y = Predef⁰.hide⁰(1); +//│ set y = (Predef⁰.hide⁰(1) as! Int⁰); //│ set res² = y; //│ end //│ else @@ -383,7 +383,7 @@ hide of print(res) //│ match scrut //│ true => //│ let y; -//│ set y = Predef⁰.hide⁰(1); +//│ set y = (Predef⁰.hide⁰(1) as! Int⁰); //│ set res² = y; //│ end //│ else diff --git a/hkmc2/shared/src/test/mlscript/opt/CaseOfCase.mls b/hkmc2/shared/src/test/mlscript/opt/CaseOfCase.mls index 3744bd7c49..f4b5f6b5a6 100644 --- a/hkmc2/shared/src/test/mlscript/opt/CaseOfCase.mls +++ b/hkmc2/shared/src/test/mlscript/opt/CaseOfCase.mls @@ -196,7 +196,7 @@ fun prefixDependency(x) = //│ else //│ set tmp = PrefixBox⁰(false); //│ end -//│ set copy = tmp.value¹; +//│ set copy = (tmp.value¹ as! Bool⁰); //│ match tmp //│ PrefixBox¹ => //│ return copy diff --git a/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls b/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls index c95806fb56..d887259cb8 100644 --- a/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls +++ b/hkmc2/shared/src/test/mlscript/opt/ConstantFolding.mls @@ -70,7 +70,7 @@ let last = peek set peek = 1 last //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ set peek⁰ = Predef⁰.hide⁰(4); set last⁰ = peek⁰; set peek⁰ = 1; return last⁰ +//│ set peek⁰ = (Predef⁰.hide⁰(4) as! Int⁰); set last⁰ = peek⁰; set peek⁰ = 1; return last⁰ //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 4 //│ last = 4 diff --git a/hkmc2/shared/src/test/mlscript/opt/DeadAssignmentRemoval.mls b/hkmc2/shared/src/test/mlscript/opt/DeadAssignmentRemoval.mls index 276b14aa89..fec65a51ab 100644 --- a/hkmc2/shared/src/test/mlscript/opt/DeadAssignmentRemoval.mls +++ b/hkmc2/shared/src/test/mlscript/opt/DeadAssignmentRemoval.mls @@ -13,7 +13,7 @@ in set x = hide(2) print(x) //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let tmp; set tmp = Predef⁰.hide⁰(2); return Predef⁰.print⁰(tmp) +//│ let x, tmp; set tmp = Predef⁰.hide⁰(2); set x = (tmp as! Int⁰); return Predef⁰.print⁰(x) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ > 2 @@ -27,13 +27,16 @@ in set x += 1 print(x) //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let tmp, tmp1, tmp2; +//│ let x, tmp, tmp1, tmp2, tmp3; //│ set tmp = Predef⁰.hide⁰(2); -//│ do Predef⁰.print⁰(tmp); -//│ do Predef⁰.hide⁰(3); -//│ set tmp1 = Predef⁰.hide⁰(4); -//│ set tmp2 = +⁰(tmp1, 1); -//│ return Predef⁰.print⁰(tmp2) +//│ set x = (tmp as! Int⁰); +//│ do Predef⁰.print⁰(x); +//│ set tmp1 = Predef⁰.hide⁰(3); +//│ do (tmp1 as! Int⁰); +//│ set tmp2 = Predef⁰.hide⁰(4); +//│ set x = (tmp2 as! Int⁰); +//│ set tmp3 = +⁰(x, 1); +//│ return Predef⁰.print⁰(tmp3) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ > 2 //│ > 5 diff --git a/hkmc2/shared/src/test/mlscript/opt/DeadConstructorElim.mls b/hkmc2/shared/src/test/mlscript/opt/DeadConstructorElim.mls index a87aec4d1d..c4ba577406 100644 --- a/hkmc2/shared/src/test/mlscript/opt/DeadConstructorElim.mls +++ b/hkmc2/shared/src/test/mlscript/opt/DeadConstructorElim.mls @@ -3,7 +3,7 @@ :flowBasedOpt debugDce -data class A(x) +data class A(x: Any) data class B(y) data class C(z) class P(x) with @@ -185,7 +185,7 @@ app(c => 2) + app(c => if c is C then 1 else 0) //│ define app⁰ as fun app¹(k) { //│ let tmp2; //│ set tmp2 = C⁰(1); -//│ return k(tmp2) +//│ return (k(tmp2) as! Int⁰) //│ }; //│ define lambda as fun lambda⁰(c) { //│ return 2 @@ -225,7 +225,7 @@ app(c => 2) + app(c => if c is C then 1 else 0) //│ @private //│ define app$app as fun app$app⁰(k) { //│ do app_ctor$ctor⁰(); -//│ return k() +//│ return (k() as! Int⁰) //│ }; //│ @private //│ define app_ctor$ctor as fun app_ctor$ctor⁰() { @@ -237,7 +237,7 @@ app(c => 2) + app(c => if c is C then 1 else 0) //│ define app² as fun app³(k) { //│ let tmp2; //│ set tmp2 = ctor¹(); -//│ return k(tmp2) +//│ return (k(tmp2) as! Int⁰) //│ }; //│ set tmp = app$app⁰(lambda$lambda⁰); //│ define lambda as fun lambda²(c) { diff --git a/hkmc2/shared/src/test/mlscript/opt/EtaExpansion.mls b/hkmc2/shared/src/test/mlscript/opt/EtaExpansion.mls index 6527435cb8..3bf5c9ca1d 100644 --- a/hkmc2/shared/src/test/mlscript/opt/EtaExpansion.mls +++ b/hkmc2/shared/src/test/mlscript/opt/EtaExpansion.mls @@ -76,7 +76,7 @@ g(f, 1)(2) + g(h, 3)(4) //│ return *⁰(a, b) //│ }; //│ define g⁴ as fun g⁵(k, x) { -//│ return k(x) +//│ return (k(x) as! Function⁰) //│ }; //│ set baseCall = g⁵(f⁵, 1); //│ set tmp = baseCall(2); @@ -103,7 +103,7 @@ g(f, 1)(2) + g(h, 3)(4) //│ let tmp, baseCall, tmp1, g$g; //│ @private //│ define g$g as fun g$g⁰(k, x)(eta$0$0) { -//│ return k(x)(eta$0$0) +//│ return (k(x) as! Function⁰)(eta$0$0) //│ }; //│ @affine(1) //│ define f⁶ as fun f⁷(a)(b) { @@ -113,7 +113,7 @@ g(f, 1)(2) + g(h, 3)(4) //│ return *⁰(a, b) //│ }; //│ define g⁶ as fun g⁷(k, x) { -//│ return k(x) +//│ return (k(x) as! Function⁰) //│ }; //│ set tmp = g$g⁰(f⁷, 1)(2); //│ set baseCall = g⁷(h⁵, 3); @@ -274,7 +274,9 @@ app(returnRest2(0)) //│ return rest2¹()(...eta$0$rest) //│ }; //│ @private -//│ define app⁰ as fun app¹(f) { return f(2, 3) }; +//│ define app⁰ as fun app¹(f) { +//│ return (f(2, 3) as! Array⁰) +//│ }; //│ set tmp = returnRest2¹(); //│ return app¹(tmp) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -308,9 +310,9 @@ f(k1, 1) + f(k2, 5) //│ match scrut //│ true => //│ do Predef⁰.print⁰("g"); -//│ return k(y)(eta$0$0) +//│ return (k(y) as! Function⁰)(eta$0$0) //│ else -//│ return k(y)(eta$0$0) +//│ return (k(y) as! Function⁰)(eta$0$0) //│ end //│ }; //│ do Predef⁰.print⁰("between"); @@ -331,14 +333,14 @@ f(k1, 1) + f(k2, 5) //│ match scrut //│ true => //│ do Predef⁰.print⁰("g"); -//│ return k(y) +//│ return (k(y) as! Function⁰) //│ else -//│ return k(y) +//│ return (k(y) as! Function⁰) //│ end //│ }; //│ set h = g⁹(x); //│ do Predef⁰.print⁰("between"); -//│ return h(2) +//│ return (h(2) as! Int⁰) //│ }; //│ set tmp = f$f⁰(k1¹, 1); //│ set tmp1 = f⁹(k2¹, 5); @@ -372,7 +374,7 @@ f(k1, 1) + f(k2, 5) //│ return b⁰(y)(eta$0$0) //│ }; //│ define b as fun b⁰(y)(eta$0$0) { -//│ return k(y)(eta$0$0) +//│ return (k(y) as! Function⁰)(eta$0$0) //│ }; //│ return a⁰(x)(2) //│ }; @@ -389,10 +391,10 @@ f(k1, 1) + f(k2, 5) //│ return b¹(y) //│ }; //│ define b as fun b¹(y) { -//│ return k(y) +//│ return (k(y) as! Function⁰) //│ }; //│ set h = a¹(x); -//│ return h(2) +//│ return (h(2) as! Int⁰) //│ }; //│ set tmp = f$f¹(k1³, 1); //│ set tmp1 = f¹¹(k2³, 5); @@ -429,7 +431,7 @@ f(k1, 1) + f(k2, 5) //│ end //│ }; //│ define g as fun g¹⁰(y)(eta$0$0) { -//│ return k(y)(eta$0$0) +//│ return (k(y) as! Function⁰)(eta$0$0) //│ }; //│ define C as class C⁰ { //│ val x¹; @@ -460,7 +462,7 @@ f(k1, 1) + f(k2, 5) //│ end //│ }; //│ define g as fun g¹¹(y) { -//│ return k(y) +//│ return (k(y) as! Function⁰) //│ }; //│ define C as class C² { //│ val x³; @@ -481,4 +483,26 @@ f(k1, 1) + f(k2, 5) //│ = 13 - +// the params eta expansion appends take the types of the params they stand for, and the result is what the targets +// return: `g(x: Int)(eta$0$0: Int): Int` passes `eta$0$0` to `b: Int` without a cast +:soir +:siret +:noInline +:expect 3 +@affine(1) fun f(a: Int)(b: Int) = a + b +fun g(x) = f(x) +g(1)(2) +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ let g$g; +//│ @private +//│ define g$g as fun g$g¹(x: Int⁰)(eta$0$0: Int⁰): Int⁰ { +//│ return f¹⁴(x)(eta$0$0) +//│ }; +//│ @affine(1) +//│ define f¹⁵ as fun f¹⁴(a: Int⁰)(b: Int⁰): Int⁰ { +//│ return +⁰(a, b) +//│ }; +//│ define g¹² as fun g¹³(x: Int⁰)(eta$0$0: Int⁰): Int⁰ { return f¹⁴(x)(eta$0$0) }; +//│ return g$g¹(1)(2) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 3 diff --git a/hkmc2/shared/src/test/mlscript/opt/IllFormedAccesses.mls b/hkmc2/shared/src/test/mlscript/opt/IllFormedAccesses.mls index 432acb1afd..66e96b8c99 100644 --- a/hkmc2/shared/src/test/mlscript/opt/IllFormedAccesses.mls +++ b/hkmc2/shared/src/test/mlscript/opt/IllFormedAccesses.mls @@ -46,7 +46,7 @@ let x = unsafe.undefined in print(x) //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let x, scrut; -//│ set x = undefined; +//│ set x = (undefined as!! Int⁰); //│ set scrut = Predef⁰.hide⁰(false); //│ match scrut //│ true => diff --git a/hkmc2/shared/src/test/mlscript/opt/NoCaseOfCase.mls b/hkmc2/shared/src/test/mlscript/opt/NoCaseOfCase.mls index dd7253edb9..e65fbf6aff 100644 --- a/hkmc2/shared/src/test/mlscript/opt/NoCaseOfCase.mls +++ b/hkmc2/shared/src/test/mlscript/opt/NoCaseOfCase.mls @@ -41,7 +41,7 @@ fun foo(x) = //│ set y = w; //│ return runtime⁰.Unit⁰ //│ }; -//│ set f = Predef⁰.hide⁰(lambda⁰); +//│ set f = (Predef⁰.hide⁰(lambda⁰) as! Function⁰); //│ match x //│ true => //│ set y = "ok"; diff --git a/hkmc2/shared/src/test/mlscript/opt/PureCallPropagation.mls b/hkmc2/shared/src/test/mlscript/opt/PureCallPropagation.mls index 2876ff84b0..10756e4c41 100644 --- a/hkmc2/shared/src/test/mlscript/opt/PureCallPropagation.mls +++ b/hkmc2/shared/src/test/mlscript/opt/PureCallPropagation.mls @@ -171,7 +171,7 @@ fun scopedPrefixFoo(x)(y) = x + y //│ end //│ true => //│ let x; -//│ set x = Predef⁰.hide⁰(0); +//│ set x = (Predef⁰.hide⁰(0) as! Int⁰); //│ set f = scopedPrefixFoo¹(x); //│ end //│ else @@ -183,3 +183,30 @@ fun scopedPrefixFoo(x)(y) = x + y //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = fun + + +// A checked argument stays at the partial application site; the remaining pure +// call prefix can still combine after the source variable changes. +:expect 13 +:soir +:inlineThreshold 0 +private fun checkedPrefix(x: Int)(y: Int): Int = x + y +private fun capturePrefix(n: Any) = + let f = checkedPrefix(n) + set n = 20 + f(3) +capturePrefix(10) +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ @inline @private +//│ define checkedPrefix⁰ as fun checkedPrefix¹(x)(y) { +//│ return +⁰(x, y) +//│ }; +//│ @private +//│ define capturePrefix⁰ as fun capturePrefix¹(n) { +//│ let arg; +//│ set arg = (n as! Int⁰); +//│ return +⁰(arg, 3) +//│ }; +//│ return capturePrefix¹(10) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 13 diff --git a/hkmc2/shared/src/test/mlscript/slop/Erasure.mls b/hkmc2/shared/src/test/mlscript/slop/Erasure.mls new file mode 100644 index 0000000000..92b0286fb4 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/Erasure.mls @@ -0,0 +1,166 @@ +:. +#lang(0.3.x) +open annotations + +// A generator override's callable returns Iterator, but its body's payload can have any representation. +:noInline +:sir +:siret +:expect {value: "payload", done: true} +private class GeneratorBase() with + @annotations.generator + open fun generate(value: Any): Any = value +private class GeneratorChild() extends GeneratorBase() with + @annotations.generator + override fun generate(value: Any) = value +GeneratorChild().generate("payload").next() +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp: GeneratorChild⁰, tmp1: Iterator⁰; +//│ @private +//│ define GeneratorBase⁰ as class GeneratorBase² { +//│ constructor GeneratorBase¹ +//│ method generate⁰ = fun generate¹(value: Unknown): Iterator⁰ { +//│ return value +//│ } +//│ }; +//│ @private +//│ define GeneratorChild¹ as class GeneratorChild⁰ extends GeneratorBase² { +//│ constructor GeneratorChild² +//│ method generate² = fun generate³(value: Unknown): Iterator⁰ { +//│ return value +//│ } +//│ }; +//│ set tmp = GeneratorChild²(); +//│ set tmp1 = tmp.generate³("payload"); +//│ return tmp1.next⁰() +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = {value: "payload", done: true} + + +// Removing a checked cast can expose a subclass producer to fusion. Its base-class arm must still match. +:noInline +:lift +:deforest +:expect 1 +private class Parent() +private class Child(value: Int) extends Parent() +private fun classify(value: Any) = + val parent: Parent = value + if parent is Parent then 1 else 0 +classify(Child(42)) +//│ deforest > >>> fusing >>> +//│ deforest > Child⁰(42) -> +//│ deforest > match: scrut⁰ +//│ deforest > <<< fusing <<< +//│ = 1 + + +// Tuple producers have Array's class identity and the implicit Object supertype. +:noInline +:lift +:deforest +:expect [1, 1, 1] +private fun arrayKind(value: Any) = if value is Array then 1 else 0 +private fun objectKind(value: Any) = if value is Object then 1 else 0 +[arrayKind([1, 2]), objectKind([3, 4]), objectKind(Child(0))] +//│ deforest > >>> fusing >>> +//│ deforest > Child⁰(0) -> +//│ deforest > match: value⁰ +//│ deforest > [3, 4] -> +//│ deforest > match: value⁰ +//│ deforest > [1, 2] -> +//│ deforest > match: value¹ +//│ deforest > <<< fusing <<< +//│ = [1, 1, 1] + + +// Default branches must specialize their field parameters separately for constructors of different arities. +:noInline +:lift +:deforest +:expect [0, 0] +private class A(v: Int) +private class B() +private class Target() +private fun make(flag: Bool) = if flag then A(1) else B() +private fun fallback(value: Any) = if value is Target then 1 else 0 +[fallback(make(true)), fallback(make(false))] +//│ deforest > >>> fusing >>> +//│ deforest > A⁰(1) -> +//│ deforest > match: value² +//│ deforest > B⁰() -> +//│ deforest > match: value² +//│ deforest > A⁰(1) -> +//│ deforest > match: value² +//│ deforest > B⁰() -> +//│ deforest > match: value² +//│ deforest > <<< fusing <<< +//│ = [0, 0] + + +// Passing a checked cast does not justify dropping the constructor body and its effects. +:noInline +:lift +:deforest +:expect [1, 1] +let castCount = 0 +private class CastEffect(value: Int) with + set castCount = castCount + 1 +private fun consumeCastEffect(value: Any) = + val instance: CastEffect = value + if instance is CastEffect then 1 else 0 +[consumeCastEffect(CastEffect(42)), castCount] +//│ deforest > >>> fusing >>> +//│ deforest > <<< fusing <<< +//│ = [1, 1] +//│ castCount = 1 + +// A harmless subclass body cannot justify dropping effects in its superclass constructor. +:noInline +:lift +:deforest +:expect [1, 1] +let inheritedCount = 0 +private class EffectfulParent() with + set inheritedCount = inheritedCount + 1 +private class EffectfulChild(value: Int) extends EffectfulParent() +private fun consumeInheritedEffect(value: Any) = + val instance: EffectfulChild = value + if instance is EffectfulChild then 1 else 0 +[consumeInheritedEffect(EffectfulChild(42)), inheritedCount] +//│ deforest > >>> fusing >>> +//│ deforest > <<< fusing <<< +//│ = [1, 1] +//│ inheritedCount = 1 + +// Constructor-local writes can change a projected parameter field even without external effects. +:noInline +:lift +:deforest +:expect 2 +private class ChangedField(mut val value: Int, replacement: Int) with + set this.value = replacement +private fun readChanged(value: Any) = + val instance: ChangedField = value + instance.value +readChanged(ChangedField(1, 2)) +//│ deforest > >>> fusing >>> +//│ deforest > <<< fusing <<< +//│ = 2 + +// Lifting can add constructor captures beyond the source parameter list. +:noInline +:lift +:deforest +:expect 2 +private fun readCaptured(captured: Int) = + class Captured(val value: Int) with + fun capture() = captured + fun read(value: Any) = + val instance: Captured = value + instance.value + read(Captured(2)) +readCaptured(1) +//│ deforest > >>> fusing >>> +//│ deforest > <<< fusing <<< +//│ = 2 diff --git a/hkmc2/shared/src/test/mlscript/slop/OptionalBinding.mls b/hkmc2/shared/src/test/mlscript/slop/OptionalBinding.mls deleted file mode 100644 index 431c31589e..0000000000 --- a/hkmc2/shared/src/test/mlscript/slop/OptionalBinding.mls +++ /dev/null @@ -1,138 +0,0 @@ -:. -open annotations - -// Missing optional arguments bind null. Unsafe calls below remain regression probes; -// their observed outputs do not define semantics for unsafe.undefined. -:noInline -fun absent(x?) = x is null -[absent(), absent(unsafe.undefined), absent(null), absent(()), absent(0), absent(false)] -//│ = [true, true, true, false, false, false] - -// Known omissions are explicit null arguments in the shared IR. -:sir -:noInline -:expect true -absent() -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ return absent⁰(null) -//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ = true - -// Optional defaults also apply through function values and lambdas. -:noInline -let callback = absent -let lambda = (x?) => x is null -[callback(), callback(unsafe.undefined), lambda(), lambda(unsafe.undefined)] -//│ = [true, true, true, true] -//│ callback = fun absent -//│ lambda = fun lambda - -// Each curried parameter list supplies its own defaults. -:noInline -fun curried(a, b, c)(x?) = x is null -[curried(1, 2, 3)(), curried(1, 2, 3)(unsafe.undefined), curried(1, 2, 3)(null), curried(1, 2, 3)(())] -//│ = [true, true, true, false] - -// Inlining binds omitted slots before substituting parameters into the body. -@inline private fun inlined(x?) = x is null -[inlined(), inlined(unsafe.undefined), inlined(null), inlined(())] -//│ = [true, false, true, false] - -// Fixed optional slots precede the rest tuple, including calls through spreads. -:noInline -fun withRest(x?, ...xs) = [x is null, xs.length] -fun fromSpread(xs: Array[Any]) = withRest(...xs) -[withRest(), withRest(unsafe.undefined), withRest(0, 1, 2), fromSpread([]), fromSpread([unsafe.undefined])] -//│ = [[true, 0], [true, 0], [false, 2], [true, 0], [true, 0]] - -// Optional slots must not consume required trailing arguments. -:noInline -fun trailing(x?, ...xs, last) = [x is null, xs.length, last] -fun trailingSpread(xs: Array[Any]) = trailing(...xs, 3) -[trailing(3), trailing(unsafe.undefined, 3), trailing(0, 2, 3), trailingSpread([]), trailingSpread([unsafe.undefined])] -//│ = [[true, 0, 3], [false, 0, 3], [false, 1, 3], [true, 0, 3], [false, 0, 3]] - -// Tail-call jumps bind null again when the next iteration omits an earlier value. -@tailrec fun omit(n, x?) = if n == 0 then x is null else omit(n - 1) -@tailrec fun keep(n, x?) = if n == 0 then x is null else keep(n - 1, x) -@tailrec fun spreadOmit(n, x?) = if n == 0 then x is null else spreadOmit(...[n - 1]) -@tailrec fun spreadKeep(n, x?) = if n == 0 then x is null else spreadKeep(...[n - 1, x]) -[omit(2, 1), keep(2, unsafe.undefined), spreadOmit(2, 1), spreadKeep(2, unsafe.undefined)] -//│ = [true, true, true, true] - -// Constructor flattening preserves empty slots between parameter lists. -class Fields(val first?)(val second?) { - constructor(val last) - fun values() = [first is null, second is null, last] -} -fun build(xs: Array[Any]) = new Fields(...xs)(2)(4) -[new Fields()(2)(4).values(), new Fields(unsafe.undefined)()(4).values(), build([]).values(), build([unsafe.undefined]).values()] -//│ = [[true, false, 4], [true, true, 4], [true, false, 4], [true, false, 4]] - -// Evaluate spread arguments once, before binding constructor fields. -class Box(val value?) -let count = 0 -fun makeArgs(present) = - set count = count + 1 - if present then [unsafe.undefined] else [] -let missing = new Box(...makeArgs(false)) -let supplied = new Box(...makeArgs(true)) -[missing.value is null, supplied.value is null, count] -//│ = [true, true, 2] -//│ count = 2 -//│ missing = Box(null) -//│ supplied = Box(null) - -// Eliminating constructors must still bind missing fields to null. -:lift -:deforest -data class Data(value?) -private fun read(d: Data) = d.value is null -[read(Data()), read(Data(unsafe.undefined)), read(Data(()))] -//│ deforest > >>> fusing >>> -//│ deforest > <<< fusing <<< -//│ = [true, true, false] - -// Defined values behave consistently across calls, inlining, spreads, and tail-call jumps. -:expect [true, true, false, false, false, true, true, true, true] -[absent(), absent(null), absent(()), absent(0), absent(false), inlined(), fromSpread([]).0, omit(2, 1), spreadOmit(2, 1)] -//│ = [true, true, false, false, false, true, true, true, true] - -// Optional trailing slots and constructor fields retain null when calls are rewritten. -:expect [[true, 0, 3], [true, 0, 3], true, [true, true, 4]] -[trailing(3), trailingSpread([]), read(Data()), new Fields()()(4).values()] -//│ = [[true, 0, 3], [true, 0, 3], true, [true, true, 4]] - -// Merged dispatchers reset omitted parameters on each transition. -:expect [true, true] -@tailrec fun even(n, x?) = if n == 0 then x is null else odd(n - 1) -@tailrec fun odd(n, x?) = if n == 0 then x is null else even(n - 1) -[even(3, 9), odd(3, 10)] -//│ = [true, true] - -// Closures retain each iteration's value, including a newly omitted slot. -:expect [null, 1, 2] -@tailrec fun captured(n, fs: Array[() -> Any], x?) = - if n == 0 then [(() => x), ...fs].map(f => f()) - else if n == 1 then captured(0, [(() => x), ...fs]) - else captured(n - 1, [(() => x), ...fs], n - 1) -captured(2, [], 2) -//│ = [null, 1, 2] - -// Scoped groups retain both captured values and optional defaults after inlining. -:expect [[10, null], [10, 11]] -let - offset = 10 - select(value?) = [offset, value] -in [select(), select(11)] -//│ = [[10, null], [10, 11]] - -// Constructor slot extraction evaluates the source tuple once. -:expect [null, 23, 2] -let calls = 0 -fun values(present) = - set calls = calls + 1 - if present then [23] else [] -[new Box(...values(false)).value, new Box(...values(true)).value, calls] -//│ = [null, 23, 2] -//│ calls = 2 diff --git a/hkmc2/shared/src/test/mlscript/slop/OptionalConstructors.mls b/hkmc2/shared/src/test/mlscript/slop/OptionalConstructors.mls index 8207f6bf35..6f4b806c76 100644 --- a/hkmc2/shared/src/test/mlscript/slop/OptionalConstructors.mls +++ b/hkmc2/shared/src/test/mlscript/slop/OptionalConstructors.mls @@ -54,7 +54,7 @@ finish()().values() //│ finish = fun // Omission supplies null to the constructor body and to mutable fields. -class Mutable(mut val value?) { +class Mutable(mut val value?: Int) { val initiallyMissing = value is null } diff --git a/hkmc2/shared/src/test/mlscript/slop/UnsafeUndefined.mls b/hkmc2/shared/src/test/mlscript/slop/UnsafeUndefined.mls index e3e1b04257..2b32de8fbd 100644 --- a/hkmc2/shared/src/test/mlscript/slop/UnsafeUndefined.mls +++ b/hkmc2/shared/src/test/mlscript/slop/UnsafeUndefined.mls @@ -10,7 +10,8 @@ undefined fun oops = unsafe.undefined -// * This used to throw, but we now emit `Unit` upon compiling MIR's `return undefined` +// * `unsafe.undefined` is an unspecified value (see Runtime Representations in `doc/reference.md`); +// * the JS backend compiles `return undefined` to `return runtime.Unit`. :noInline print(oops) //│ > () @@ -21,14 +22,14 @@ print(oops) fun bareValue() = [undefined] bareValue() //│ ╔══[WARNING] Direct use of 'undefined' is discouraged; use 'unsafe.undefined' for JavaScript interoperation. -//│ ║ l.21: fun bareValue() = [undefined] +//│ ║ l.22: fun bareValue() = [undefined] //│ ╙── ^^^^^^^^^ //│ = [undefined] :w type BareType = undefined //│ ╔══[WARNING] Direct use of 'undefined' is discouraged; use 'unsafe.undefined' for JavaScript interoperation. -//│ ║ l.29: type BareType = undefined +//│ ║ l.30: type BareType = undefined //│ ╙── ^^^^^^^^^ :w @@ -36,7 +37,7 @@ type BareType = undefined fun barePattern(value) = if value is undefined then 1 else 0 [barePattern(unsafe.undefined), barePattern(null)] //│ ╔══[WARNING] Direct use of 'undefined' is discouraged; use 'unsafe.undefined' for JavaScript interoperation. -//│ ║ l.36: fun barePattern(value) = if value is undefined then 1 else 0 +//│ ║ l.37: fun barePattern(value) = if value is undefined then 1 else 0 //│ ╙── ^^^^^^^^^ //│ = [1, 0] @@ -47,7 +48,7 @@ fun bareLength(value: Str | BareType) = if value is ~undefined as text then text.length else 0 [bareLength(unsafe.undefined), bareLength("abc")] //│ ╔══[WARNING] Direct use of 'undefined' is discouraged; use 'unsafe.undefined' for JavaScript interoperation. -//│ ║ l.47: if value is ~undefined as text then text.length else 0 +//│ ║ l.48: if value is ~undefined as text then text.length else 0 //│ ╙── ^^^^^^^^^ //│ = [0, 3] @@ -57,19 +58,13 @@ fun bareLength(value: Str | BareType) = :expect [undefined, true, false] [unsafe.undefined, unsafe.undefined === unsafe.undefined, unsafe.undefined === null] //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ return [undefined, true, false] +//│ return [(undefined as!! Unknown), true, false] //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ return globalThis.Object.freeze([ undefined, true, false ]) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = [undefined, true, false] // Runtime patterns still distinguish undefined from legitimate values. -:expect [0, 3] -fun length(value: Str | unsafe.undefined) = - if value is ~unsafe.undefined as text then text.length else 0 -[length(unsafe.undefined), length("abc")] -//│ = [0, 3] - :expect [true, false] fun isUndefined(value) = if value is unsafe.undefined then true else false [isUndefined(unsafe.undefined), isUndefined(null)] @@ -157,10 +152,10 @@ projected(Box("abc")) :e fun wrongArity(f: (Int -> Str) | unsafe.undefined) = f() //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.158: fun wrongArity(f: (Int -> Str) | unsafe.undefined) = f() +//│ ║ l.153: fun wrongArity(f: (Int -> Str) | unsafe.undefined) = f() //│ ║ ^^^ //│ ╟── Function with a declared signature expected 1 argument, but got 0 -//│ ║ l.158: fun wrongArity(f: (Int -> Str) | unsafe.undefined) = f() +//│ ║ l.153: fun wrongArity(f: (Int -> Str) | unsafe.undefined) = f() //│ ╙── ^^^^^^^^^^ // Generic forwarding retains the nullable interface; the unsafe call remains a regression probe. @@ -207,20 +202,20 @@ let record = {absent: unsafe.undefined, text: "abc"} private fun optionalLength(value?) = value.length fun forward(value: Str | unsafe.undefined) = optionalLength(value) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.207: private fun optionalLength(value?) = value.length +//│ ║ l.202: private fun optionalLength(value?) = value.length //│ ║ ^^^^^^^^^^^^ //│ ╟── Null literal does not contain member 'length' -//│ ║ l.207: private fun optionalLength(value?) = value.length +//│ ║ l.202: private fun optionalLength(value?) = value.length //│ ╙── ^^^^^^ // The optional parameter's null flow also survives generic result substitution. :e fun unguardedGeneric(value: Str | unsafe.undefined) = genericIdentity(value).length //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.218: fun unguardedGeneric(value: Str | unsafe.undefined) = genericIdentity(value).length +//│ ║ l.213: fun unguardedGeneric(value: Str | unsafe.undefined) = genericIdentity(value).length //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Null literal does not contain member 'length' -//│ ║ l.168: private fun optionalIdentity(value?) = value +//│ ║ l.163: private fun optionalIdentity(value?) = value //│ ╙── ^^^^^^ @@ -233,13 +228,13 @@ class Right(val value: Str) :e fun ambiguous(value: Left | unsafe.undefined | Right) = value.value //│ ╔══[COMPILATION ERROR] This selection of member 'value' is ambiguous, as it has multiple resolved targets -//│ ║ l.234: fun ambiguous(value: Left | unsafe.undefined | Right) = value.value +//│ ║ l.229: fun ambiguous(value: Left | unsafe.undefined | Right) = value.value //│ ║ ^^^^^^^^^^^ //│ ╟── target: value 'value' in overloaded function and class 'Right' -//│ ║ l.231: class Right(val value: Str) +//│ ║ l.226: class Right(val value: Str) //│ ║ ^^^^^ //│ ╟── target: value 'value' in overloaded function and class 'Left' -//│ ║ l.230: class Left(val value: Str) +//│ ║ l.225: class Left(val value: Str) //│ ╙── ^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/slop/VariadicParams.mls b/hkmc2/shared/src/test/mlscript/slop/VariadicParams.mls index 3a988525f5..7c7149b8c6 100644 --- a/hkmc2/shared/src/test/mlscript/slop/VariadicParams.mls +++ b/hkmc2/shared/src/test/mlscript/slop/VariadicParams.mls @@ -9,13 +9,14 @@ fun f(x, ...xs, y) = [x, xs, y] //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let f; //│ f = function f(x, ...args) { -//│ let y, inlinedVal, tmp, tmp1; +//│ let xs, y, inlinedVal, tmp, tmp1; //│ tmp1 = 1 + args.length; //│ runtime.checkArgs("f", 2, tmp1); //│ tmp = args.length - 1; //│ inlinedVal = runtime.safeCall(args.slice(0, tmp)); +//│ xs = inlinedVal; //│ y = args.at(-1); -//│ return globalThis.Object.freeze([ x, inlinedVal, y ]) +//│ return globalThis.Object.freeze([ x, xs, y ]) //│ }; //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -26,7 +27,7 @@ fun f(x, ...xs, y) = [x, xs, y] :e f(1) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.27: f(1) +//│ ║ l.28: f(1) //│ ║ ^^^^ //│ ╟── Function 'f' expected at least 2 arguments, but got 1 //│ ║ l.8: fun f(x, ...xs, y) = [x, xs, y] @@ -53,7 +54,7 @@ fun h(x, y?, z?, ...xs, w) = [x, y, z, xs, w] //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let h; //│ h = function h(x, ...args7) { -//│ let y7, z2, w, inlinedVal12, inlinedVal13, tmp14, tmp15, tmp16; +//│ let y7, z2, xs7, w, inlinedVal12, inlinedVal13, tmp14, tmp15, tmp16; //│ tmp16 = 1 + args7.length; //│ runtime.checkArgs("h", 2, tmp16); //│ tmp15 = args7.length - 1; @@ -70,8 +71,9 @@ fun h(x, y?, z?, ...xs, w) = [x, y, z, xs, w] //│ } //│ tmp14 = args7.length - 1; //│ inlinedVal13 = runtime.safeCall(args7.slice(2, tmp14)); +//│ xs7 = inlinedVal13; //│ w = args7.at(-1); -//│ return globalThis.Object.freeze([ x, y7, z2, inlinedVal13, w ]) +//│ return globalThis.Object.freeze([ x, y7, z2, xs7, w ]) //│ }; //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -133,10 +135,10 @@ lastA(B(0), B(1), A(2)) private fun lastB(...xs, y) = y.b lastB(B(0), A(1)) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.133: private fun lastB(...xs, y) = y.b +//│ ║ l.135: private fun lastB(...xs, y) = y.b //│ ║ ^^^ //│ ╟── Instance of class 'A' does not contain member 'b' -//│ ║ l.134: lastB(B(0), A(1)) +//│ ║ l.136: lastB(B(0), A(1)) //│ ╙── ^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -229,34 +231,110 @@ private fun optional(x?, ...middle, y) = [if x is Head as x then x.head else uns //│ = [[undefined, 7], [8, 9]] +// *** Erased representations of extracted parameters *** + +// Known arguments are checked against trailing slots, counting from the right. +:ge +:noInline +:sir +:siret +private fun integerTail(...rest, value: Int) = value +integerTail(0, 1, "wrong") +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ║ l.242: integerTail(0, 1, "wrong") +//│ ╙── ^^^^^^^ +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ @private +//│ define integerTail⁰ as fun integerTail¹(...args: Array⁰): Int⁰ { +//│ let rest: Array⁰, value: Int⁰; +//│ do runtime⁰.checkVariadicArgs⁰("integerTail", 1, 0, args); +//│ set rest = (runtime⁰.Tuple⁰.slice⁰(args, 0, 1) as! Array⁰); +//│ set value = (args.-1 as! Int⁰); +//│ return value +//│ }; +//│ return integerTail¹(0, 1, "wrong") +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = "wrong" + +// Optional leading slots yield to both typed trailing slots before their representations are checked. +:expect [[null, "A", 1], [2, "B", 3], [4, "C", 5]] +private fun typedLayout(head?: Int, ...rest, text: Str, count: Int) = [head, text, count] +[typedLayout("A", 1), typedLayout(2, "B", 3), typedLayout(4, false, "C", 5)] +//│ = [[null, "A", 1], [2, "B", 3], [4, "C", 5]] + +// Unknown-length calls still coerce the dynamically extracted value into its typed destination. +:noInline +:expect 7 +fun forwardedTail(values: Array[Int]) = integerTail(...values) +forwardedTail([7]) +//│ = 7 + + +// Trailing parameters obey the same override representation checks as leading parameters. +:ge +private class TailBase() with + open fun apply(...rest, value: Int): Int = 0 +private class TailChild() extends TailBase() with + override fun apply(...rest, value: Str): Int = 0 +private fun useTailBase(base: TailBase) = base.apply(1) +useTailBase(TailChild()) +//│ ╔══[COMPILATION ERROR] Parameter 'value' of overriding member 'apply' is represented as 'Str', which cannot hold the values represented as 'Int' that the member it overrides accepts +//│ ║ l.278: override fun apply(...rest, value: Str): Int = 0 +//│ ║ ^^^^^^^^^^ +//│ ╟── The overridden member 'apply' is defined here +//│ ║ l.276: open fun apply(...rest, value: Int): Int = 0 +//│ ╙── ^^^^^ +//│ = 0 + +// An unannotated trailing parameter inherits the base member's representation, even across blocks. +:ge +private class InheritedTail() extends TailBase() with + override fun apply(...rest, value): Int = 0 +InheritedTail().apply("wrong") +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ║ l.293: InheritedTail().apply("wrong") +//│ ╙── ^^^^^^^ +//│ = 0 + +// TODO: Decide which changes to an override's parameter layout are permitted. Adding a required +// trailing slot currently makes a valid call through the base member fail at run time. +:ge +:breakme +:re +private class ExtraTail() extends TailBase() with + override fun apply(...rest, first: Int, value: Int): Int = 0 +useTailBase(ExtraTail()) +//│ ═══[RUNTIME ERROR] Error: Function 'apply' expected at least 2 arguments but got 1 + + // *** Errors *** :e fun bad(...xs, ...ys) = 0 //│ ╔══[COMPILATION ERROR] A parameter list cannot have more than one variadic parameter. -//│ ║ l.235: fun bad(...xs, ...ys) = 0 +//│ ║ l.313: fun bad(...xs, ...ys) = 0 //│ ╙── ^^^^^ :e fun bad(...xs, y?) = 0 //│ ╔══[COMPILATION ERROR] Optional parameters cannot follow a variadic parameter. -//│ ║ l.241: fun bad(...xs, y?) = 0 +//│ ║ l.319: fun bad(...xs, y?) = 0 //│ ╙── ^^ :e fun bad(f: (...Array[Int], ...Array[Int]) -> Int) = 0 //│ ╔══[COMPILATION ERROR] A parameter list cannot have more than one variadic parameter. -//│ ║ l.247: fun bad(f: (...Array[Int], ...Array[Int]) -> Int) = 0 +//│ ║ l.325: fun bad(f: (...Array[Int], ...Array[Int]) -> Int) = 0 //│ ╙── ^^^^^^^^^^^^^ :e fun bad(f: (...Array[Int], Int?) -> Int) = 0 //│ ╔══[COMPILATION ERROR] Optional parameters cannot follow a variadic parameter. -//│ ║ l.253: fun bad(f: (...Array[Int], Int?) -> Int) = 0 +//│ ║ l.331: fun bad(f: (...Array[Int], Int?) -> Int) = 0 //│ ╙── ^^^^ :e class C(x, ...xs, y) //│ ╔══[COMPILATION ERROR] Spread parameters are not supported in class parameters. -//│ ║ l.259: class C(x, ...xs, y) +//│ ║ l.337: class C(x, ...xs, y) //│ ╙── ^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/slop/handlers/VariadicResumption.mls b/hkmc2/shared/src/test/mlscript/slop/handlers/VariadicResumption.mls index 2d62498eea..fbf68b7f74 100644 --- a/hkmc2/shared/src/test/mlscript/slop/handlers/VariadicResumption.mls +++ b/hkmc2/shared/src/test/mlscript/slop/handlers/VariadicResumption.mls @@ -7,7 +7,7 @@ // * which must include the arguments of its rest parameter. abstract class Effect with - fun perform(arg): Any + fun perform(arg: Any): Any :expect [2, 1, [2, 3]] handle h = Effect with diff --git a/hkmc2/shared/src/test/mlscript/slop/loose/.mls b/hkmc2/shared/src/test/mlscript/slop/loose/.mls new file mode 100644 index 0000000000..7e0f4b5167 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/loose/.mls @@ -0,0 +1,3 @@ +:.. +#lang(strictResolution: false) +#lang(eagerResolution: false) diff --git a/hkmc2/shared/src/test/mlscript/slop/loose/ErasureAcrossBlocks.mls b/hkmc2/shared/src/test/mlscript/slop/loose/ErasureAcrossBlocks.mls new file mode 100644 index 0000000000..5ca4f9b000 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/loose/ErasureAcrossBlocks.mls @@ -0,0 +1,83 @@ +:. + +// Worksheet blocks are compiled one after the other, and the representations erasure infers for a block's +// definitions are final once it is compiled. Without strict resolution, a block does not expose its public +// definitions to unknown callers (see `Elaborator.topLevel`), so their representations are inferred from the +// uses in their own block only, and later blocks must use them consistently. Compiled files are not affected: +// a compilation unit is elaborated with its exports exposed. + + +fun ident(x) = x +ident(1) +//│ = 1 + +// The parameter of `ident` was inferred as an `Int` from the block above. +:ge +ident("a") +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ║ l.16: ident("a") +//│ ╙── ^^^ +//│ = "a" + + +// Later blocks cannot widen the representation of a top-level `let`, which is not exposed to their writes +// even under strict resolution. +let counter = 1 +//│ counter = 1 + +:ge +set counter = "hello" +counter +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ║ l.29: set counter = "hello" +//│ ╙── ^^^^^^^^^^^^^^^^^ +//│ = "hello" + + +// The representation of a mutable field is inferred from its initializer and from the values that its own block +// assigns to it, which later blocks cannot widen either. Without strict resolution, its class is not exposed. +class Cell(mut val value) +let cell = Cell(1) +set cell.value = 2 +//│ cell = Cell(2) + +:ge +set cell.value = "hello" +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ║ l.45: set cell.value = "hello" +//│ ╙── ^^^^^^^^^^ + +class Mixed(mut val value) +let mixed = Mixed(1) +set mixed.value = "hello" +//│ mixed = Mixed("hello") + +set mixed.value = "again" + +// The same holds for the fields of mutable records. +let record = mut {count: 0} +//│ record = {count: 0} + +:ge +set record.count = "many" +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ║ l.62: set record.count = "many" +//│ ╙── ^^^^^^^^^^^^ + +let fields = {mut count: 0, label: "count"} +//│ fields = {count: 0, label: "count"} + +:ge +set fields.count = "many" +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ║ l.71: set fields.count = "many" +//│ ╙── ^^^^^^^^^^^^ + +// The values that a record type describes can be assigned values of the types of its mutable fields. +class Holder(mut val item) +let holder = Holder(1) +fun accept(described: {mut item: Str}) = () +accept(holder) +//│ holder = Holder(1) + +set holder.item = "str" diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/MutableFieldVariance.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/MutableFieldVariance.mls new file mode 100644 index 0000000000..a5a85769e7 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/MutableFieldVariance.mls @@ -0,0 +1,373 @@ +:. + + +// The write bound is Child; the read bound Base must not be fed back into the local field. +:expect 1 +class Base +class Child(val value: Int) extends Base +private fun accept(record: {mut item: in Child out Base}) = () +private fun test() = + let record = {mut item: Child(1)} + accept(record) + record.item.value +test() +//│ = 1 + + +// Reads through the split interface expose only Base. +:ge +class Base +class Child(val value: Int) extends Base +private fun read(record: {mut item: in Child out Base}) = record.item.value +read({mut item: Child(2)}) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.21: private fun read(record: {mut item: in Child out Base}) = record.item.value +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'value' +//│ ║ l.21: private fun read(record: {mut item: in Child out Base}) = record.item.value +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// The input to Receiver.accept reverses T's polarity. Its field can receive Base, +// even though the original record has only held Child values. +:ge +class Base +class Child(val value: Int) extends Base +class Receiver[T] with + fun accept(record: {mut item: T}): () = () +private fun test() = + let receiver = (new Receiver as Receiver[in Child out Base]) + let record = {mut item: Child(3)} + receiver.accept(record) + record.item.value +test() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.43: record.item.value +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'value' +//│ ║ l.40: let receiver = (new Receiver as Receiver[in Child out Base]) +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// The corresponding immutable field admits no writes and retains its local value. +:expect 4 +class Base +class Child(val value: Int) extends Base +class Receiver[T] with + fun accept(record: {item: T}): () = () +private fun test() = + let receiver = (new Receiver as Receiver[in Child out Base]) + let record = {item: Child(4)} + receiver.accept(record) + record.item.value +test() +//│ = 4 + + +// Alias substitution must retain both occurrences of the field's type parameter. +:ge +class Base +class Child(val value: Int) extends Base +type Cell[T] = {mut item: T} +class Receiver[T] with + fun accept(record: Cell[T]): () = () +private fun test() = + let receiver = (new Receiver as Receiver[in Child out Base]) + let record = {mut item: Child(5)} + receiver.accept(record) + record.item.value +test() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.80: record.item.value +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'value' +//│ ║ l.77: let receiver = (new Receiver as Receiver[in Child out Base]) +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// In positive position the same alias admits Child writes and exposes Base reads. +:expect 6 +class Base +class Child(val value: Int) extends Base +type Cell[T] = {mut item: T} +private fun test() = + let record = {mut item: Child(6)} + let view = record as Cell[in Child out Base] + record.item.value +test() +//│ = 6 + + +// An output-only field admits no writes; Nothing must not become an unknown input. +:expect 7 +class Base +class Child(val value: Int) extends Base +private fun accept(record: {mut item: out Base}) = () +private fun test() = + let record = {mut item: Child(7)} + accept(record) + record.item.value +test() +//│ = 7 + + +// An input-only field has no usable read interface. +:ge +class Child(val value: Int) +private fun read(record: {mut item: in Child}) = record.item.value +read({mut item: Child(8)}) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.120: private fun read(record: {mut item: in Child}) = record.item.value +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Cannot resolve member 'value' of a value with unknown shape. +//│ ╟── This field specifies only an input bound. +//│ ║ l.120: private fun read(record: {mut item: in Child}) = record.item.value +//│ ║ ^^^^^^^^ +//│ ╙── Values read through it have no output bound more specific than 'Any'. +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// Writes through a split interface check callbacks against the input bound, not its read bound. +:ge +class Base +class Child(val value: Int) extends Base +private fun assigned(value) = value.value +private fun store(record: {mut callback: in (Base -> Int) out (Child -> Int)}) = + set record.callback = assigned +private fun test() = + let record = {mut callback: assigned} + store(record) + assigned(Child(9)) +test() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.137: private fun assigned(value) = value.value +//│ ║ ^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'value' +//│ ║ l.138: private fun store(record: {mut callback: in (Base -> Int) out (Child -> Int)}) = +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// A nominal mutable field uses the negative occurrence of its substituted type as its write interface. +:ge +class Base +class Child(val value: Int) extends Base +class Cell[T](mut val callback: T) +private fun assigned(value) = value.value +private fun test() = + let cell = Cell[Child -> Int](assigned) + let view = cell as Cell[in (Base -> Int) out (Child -> Int)] + set view.callback = assigned + assigned(Child(10)) +test() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.159: private fun assigned(value) = value.value +//│ ║ ^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'value' +//│ ║ l.162: let view = cell as Cell[in (Base -> Int) out (Child -> Int)] +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// Intersected mutable interfaces accept writes from either component. +:ge +class Base +class Child(val value: Int) extends Base +type Both = ({mut item: Child} & {mut item: in Base out Child}) +private fun accept(record: Both) = () +private fun test() = + let record = {mut item: Child(11)} + accept(record) + record.item.value +test() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.184: record.item.value +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'value' +//│ ║ l.179: type Both = ({mut item: Child} & {mut item: in Base out Child}) +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// Merging field reads intersects their output types, and an immutable component adds no writes. +:expect 12 +type Both = ({item: {left: Int}} & {mut item: {left: Int, right: Int}}) +private fun read(record: Both) = record.item.left + record.item.right +private fun test() = + let record = {mut item: {left: 5, right: 7}} + read(record) +test() +//│ = 12 + + +// Whole-record mutability and parenthesized bounds use the same field interval. +:expect 13 +class Base +class Child(val value: Int) extends Base +private fun accept(record: mut {item: (in Child out Base)}) = () +private fun test() = + let record = {mut item: Child(13)} + accept(record) + record.item.value +test() +//│ = 13 + + +// A field needs a write interface before it can declare an input bound. +:ge +class Base +class Child extends Base +type Immutable = {item: in Child out Base} +private fun read(record: Immutable) = record.item +read({item: new Child}) +//│ ╔══[COMPILATION ERROR] Resolution error in record literal +//│ ║ l.223: type Immutable = {item: in Child out Base} +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── A field with input/output bounds must be declared 'mut'. +//│ ║ l.223: type Immutable = {item: in Child out Base} +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^ +//│ = Child + + +// Recursive fields retain finite, substituted input and output references. +:expect 14 +type Chain[T] = {mut next: Chain[T], value: T} +private fun read[T](chain: Chain[T], count: Int): T = + if count == 0 then chain.value else read(chain.next, count - 1) +private fun test() = + let chain = {mut next: unsafe.undefined, value: 14} + set chain.next = chain + read(chain, 2) +test() +//│ = 14 + + +// Forwarding a mutable structural interface must preserve its write constraints. +:ge +class Base +class Child(val value: Int) extends Base +private fun assigned(value) = value.value +private fun store(record: {mut callback: in (Base -> Int) out (Child -> Int)}) = + fun write() = set record.callback = assigned + write() +private fun forward(record: {mut callback: in (Base -> Int) out (Child -> Int)}) = store(record) +private fun test() = + let record = {mut callback: assigned} + forward(record) + assigned(Child(15)) +test() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.252: private fun assigned(value) = value.value +//│ ║ ^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'value' +//│ ║ l.253: private fun store(record: {mut callback: in (Base -> Int) out (Child -> Int)}) = +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// Reading a declared mutable field propagates S <: T to an inferred result type. +:ge +class Base +class Child(val value: Int) extends Base +private fun read[T](record: {mut f: T}): T = record.f +private fun test() = + let record = ({mut f: Child(1)} as {mut f: Base}) + read(record).value +test() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.278: read(record).value +//│ ║ ^^^^^^^^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'value' +//│ ║ l.277: let record = ({mut f: Child(1)} as {mut f: Base}) +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// A declared mutable field also propagates T <: S back to the source's type argument. +// There is no direct field assignment here: the constraint comes from the two interfaces. +:ge +class Base +class Child(val value: Int) extends Base +class Holder[S](val record: {mut f: S}) +private fun accept(record: {mut f: Base}) = () +private fun test() = + let holder = Holder({mut f: Child(2)}) + accept(holder.record) + holder.record.f.value +test() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.299: holder.record.f.value +//│ ║ ^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'value' +//│ ║ l.295: private fun accept(record: {mut f: Base}) = () +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// Whole-record mutability has the same read constraint. +:ge +class Base +class Child(val value: Int) extends Base +private fun read[T](record: mut {f: T}): T = record.f +private fun test() = + let record = (mut {f: Child(3)} as mut {f: Base}) + read(record).value +test() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.317: read(record).value +//│ ║ ^^^^^^^^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'value' +//│ ║ l.316: let record = (mut {f: Child(3)} as mut {f: Base}) +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// Whole-record mutability has the same reverse write constraint. +:ge +class Base +class Child(val value: Int) extends Base +class Holder[S](val record: mut {f: S}) +private fun accept(record: mut {f: Base}) = () +private fun test() = + let holder = Holder(mut {f: Child(4)}) + accept(holder.record) + holder.record.f.value +test() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.337: holder.record.f.value +//│ ║ ^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'value' +//│ ║ l.333: private fun accept(record: mut {f: Base}) = () +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// An immutable destination only constrains the read; it must not widen the source's write type. +:expect 5 +class Base +class Child(val value: Int) extends Base +class Holder[S](val record: {mut f: S}) +private fun accept(record: {f: Base}) = () +private fun test() = + let holder = Holder({mut f: Child(5)}) + accept(holder.record) + holder.record.f.value +test() +//│ = 5 + + +// The same control applies when the source uses whole-record mutability. +:expect 6 +class Base +class Child(val value: Int) extends Base +class Holder[S](val record: mut {f: S}) +private fun accept(record: {f: Base}) = () +private fun test() = + let holder = Holder(mut {f: Child(6)}) + accept(holder.record) + holder.record.f.value +test() +//│ = 6 diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/MutableFields.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/MutableFields.mls new file mode 100644 index 0000000000..021c929e9e --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/MutableFields.mls @@ -0,0 +1,489 @@ +:. + +// A mutable field without a declared type takes the representation of its initializer and of the values +// assigned to it (see also ../wasm/MutableFields.mls). Other compilation units can assign values to the mutable +// fields that this compilation unit exposes, so those must declare their types: their representations must not +// depend on the values that this compilation unit happens to assign to them. + + +// A public class exposes the mutable fields of its instances, whether they are parameters or members. +:ge +class Exposed(mut val param) with + mut val member = 0 +//│ ╔══[COMPILATION ERROR] Mutable field 'param' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.11: class Exposed(mut val param) with +//│ ║ ^^^^^ +//│ ╟── 'Exposed' is exposed by this compilation unit. +//│ ║ l.11: class Exposed(mut val param) with +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Mutable field 'member' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.12: mut val member = 0 +//│ ║ ^^^^^^ +//│ ╟── 'Exposed' is exposed by this compilation unit. +//│ ║ l.11: class Exposed(mut val param) with +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +class Declared(mut val param: Int) with + mut val member: Int = 0 + +// The mutable fields of a public module are exposed too. +:ge +module Settings with + mut val verbose = false +//│ ╔══[COMPILATION ERROR] Mutable field 'verbose' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.32: mut val verbose = false +//│ ║ ^^^^^^^ +//│ ╟── 'Settings' is exposed by this compilation unit. +//│ ║ l.31: module Settings with +//│ ╙── ^^^^^^^^^^^^^^^ + +// Other compilation units cannot assign to a private field. +:expect 1 +class Counter with + private mut val count = 0 + fun increment() = + set count += 1 + count +new Counter().increment() +//│ = 1 + +// The fields of a private class are exposed by the instances that escape the compilation unit, +// including through the classes that inherit them. +:ge +private class Hidden(mut val value) +fun makeHidden() = Hidden(0) +//│ ╔══[COMPILATION ERROR] Mutable field 'value' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.53: private class Hidden(mut val value) +//│ ║ ^^^^^ +//│ ╟── This value is returned here. +//│ ║ l.54: fun makeHidden() = Hidden(0) +//│ ║ ^^^^^^^^^ +//│ ╟── 'makeHidden' is exposed by this compilation unit. +//│ ║ l.54: fun makeHidden() = Hidden(0) +//│ ╙── ^^^^^^^^^^^^^^^^ + +:ge +private class Parent(mut val value) +class Child extends Parent(0) +//│ ╔══[COMPILATION ERROR] Mutable field 'value' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.66: private class Parent(mut val value) +//│ ║ ^^^^^ +//│ ╟── This parent contributes inherited members. +//│ ║ l.67: class Child extends Parent(0) +//│ ║ ^^^^^^^^^ +//│ ╟── 'Child' is exposed by this compilation unit. +//│ ║ l.67: class Child extends Parent(0) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +// A field whose values only come from this compilation unit is not exposed. +:expect 2 +private class Local(mut val value) +private fun local() = + let cell = Local(1) + set cell.value = 2 + cell.value +local() +//│ = 2 + +// Values of unknown shape that this compilation unit receives from other compilation units +// can be assigned to a field that is not exposed, without declaring its type. +private class Store(mut val value) +private val store = Store(0) +fun put(value) = set store.value = value +//│ store = Store(0) + + +// Record fields declared `mut` can be assigned; other fields are immutable, unless their whole record is +// mutable (`mut {...}`). Reading a mutable field returns its initializer and the values assigned to it. +:expect [2, "count"] +private class Box(val value) +private fun replaceInner() = + let record = {mut inner: Box(1), label: "count"} + set record.inner = Box(2) + [record.inner.value, record.label] +replaceInner() +//│ = [2, "count"] + +// Assigning through a mutable copy of a record assigns the fields of the copied record, +// so the representations of those fields include the values assigned to them through the copy. +:expect "many" +private fun copy() = + let source = {count: 0} + let copied = mut {...source} + set copied.count = "many" + copied.count +copy() +//│ = "many" + +// Copies of a record read the current values of its mutable fields. +:expect ["many", "many"] +private fun copyMutable() = + let source = {mut count: 0} + set source.count = "many" + let copied = {...source} + [source.count, copied.count] +copyMutable() +//│ = ["many", "many"] + +// The fields of an exposed record that are mutable cannot declare types: they take the unknown representation. +val exposedRecord = {mut count: 0, label: "count"} +//│ exposedRecord = {count: 0, label: "count"} + + +// A record type can declare mutable fields too. The values it describes can be assigned values of these fields' +// types through it, so their own fields hold such values, just as the parameters of a callback hold values of the +// parameter types of its function type. Assignments through the record type need not be followed. +:expect 3 +private class Holder(mut val item) +private fun put(holder: {mut item: Str}) = set holder.item = "str" +private fun structural() = + let holder = Holder("abc") + put(holder) + holder.item.length +structural() +//│ = 3 + + +// A value stored in an escaping record is exposed even when it is assigned after the record's +// construction. Clients can access its public fields and call its functions through that record. + + +// Initializers already expose their nested mutable fields, which must declare their types. +:ge +private class InitialCell(mut val value) +fun exportInitialCell() = {mut item: InitialCell(1)} +//│ ╔══[COMPILATION ERROR] Mutable field 'value' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.153: private class InitialCell(mut val value) +//│ ║ ^^^^^ +//│ ╟── This value is stored in this record field. +//│ ║ l.154: fun exportInitialCell() = {mut item: InitialCell(1)} +//│ ║ ^^^^ +//│ ╟── This value is returned here. +//│ ║ l.154: fun exportInitialCell() = {mut item: InitialCell(1)} +//│ ║ ^^^^^^^^^^^^^^^^^^^^ +//│ ╟── 'exportInitialCell' is exposed by this compilation unit. +//│ ║ l.154: fun exportInitialCell() = {mut item: InitialCell(1)} +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^ + + +// Assignment exposes the nested mutable field just like an initializer does. +:ge +private class AssignedCell(mut val value) +fun exportAssignedCell() = + let record = {mut item: 0} + set record.item = AssignedCell(1) + record +//│ ╔══[COMPILATION ERROR] Mutable field 'value' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.171: private class AssignedCell(mut val value) +//│ ║ ^^^^^ +//│ ╟── This value is stored in this record field. +//│ ║ l.173: let record = {mut item: 0} +//│ ║ ^^^^ +//│ ╟── This value is returned here. +//│ ║ l.173: let record = {mut item: 0} +//│ ║ ^^^^^^^^ +//│ ║ l.174: set record.item = AssignedCell(1) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.175: record +//│ ║ ^^^^^^^^ +//│ ╟── 'exportAssignedCell' is exposed by this compilation unit. +//│ ║ l.172: fun exportAssignedCell() = +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^ + + +// A mutable spread copy also exposes values assigned through its copied fields. +:ge +private class CopiedCell(mut val value) +fun exportCopiedCell() = + let source = {item: 0} + let copy = mut {...source} + set copy.item = CopiedCell(1) + copy +//│ ╔══[COMPILATION ERROR] Mutable field 'value' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.196: private class CopiedCell(mut val value) +//│ ║ ^^^^^ +//│ ╟── This value is stored in this record field. +//│ ║ l.198: let source = {item: 0} +//│ ║ ^^^^ +//│ ╟── This value is returned here. +//│ ║ l.198: let source = {item: 0} +//│ ║ ^^^^^^^^ +//│ ║ l.199: let copy = mut {...source} +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.200: set copy.item = CopiedCell(1) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.201: copy +//│ ║ ^^^^^^ +//│ ╟── 'exportCopiedCell' is exposed by this compilation unit. +//│ ║ l.197: fun exportCopiedCell() = +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ + + +// An escaping callback's unannotated input must admit external callers, even if its local calls +// all pass a record containing `answer`. Initializers already produce the required diagnostic. +:e +private fun initialCallback(value) = value.answer +fun exportInitialCallback() = {mut callback: initialCallback} +initialCallback({answer: 42}) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.225: private fun initialCallback(value) = value.answer +//│ ║ ^^^^^^^^^^^^ +//│ ╟── Cannot resolve member 'answer' of a value with unknown shape. +//│ ╟── Parameter 'value' admits values of unknown shape. +//│ ║ l.225: private fun initialCallback(value) = value.answer +//│ ║ ^^^^^ +//│ ╟── This value is stored in this record field. +//│ ║ l.226: fun exportInitialCallback() = {mut callback: initialCallback} +//│ ║ ^^^^^^^^ +//│ ╟── This value is returned here. +//│ ║ l.226: fun exportInitialCallback() = {mut callback: initialCallback} +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── 'exportInitialCallback' is exposed by this compilation unit. +//│ ║ l.226: fun exportInitialCallback() = {mut callback: initialCallback} +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// A callback stored through assignment receives unknown external inputs too. +:e +private fun assignedCallback(value) = value.answer +fun exportAssignedCallback() = + let record = {mut callback: 0} + set record.callback = assignedCallback + record +assignedCallback({answer: 42}) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.249: private fun assignedCallback(value) = value.answer +//│ ║ ^^^^^^^^^^^^ +//│ ╟── Cannot resolve member 'answer' of a value with unknown shape. +//│ ╟── Parameter 'value' admits values of unknown shape. +//│ ║ l.249: private fun assignedCallback(value) = value.answer +//│ ║ ^^^^^ +//│ ╟── This value is stored in this record field. +//│ ║ l.251: let record = {mut callback: 0} +//│ ║ ^^^^^^^^ +//│ ╟── This value is returned here. +//│ ║ l.251: let record = {mut callback: 0} +//│ ║ ^^^^^^^^^^^^ +//│ ║ l.252: set record.callback = assignedCallback +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.253: record +//│ ║ ^^^^^^^^ +//│ ╟── 'exportAssignedCallback' is exposed by this compilation unit. +//│ ║ l.250: fun exportAssignedCallback() = +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// The copied field retains the paths of both returned spread records. +:ge +private class NestedCell(mut val value) +private fun original() = {item: 0} +private fun intermediate() = {...original()} +fun exportNestedCopy() = + let copy = mut {...intermediate()} + set copy.item = NestedCell(1) + copy +//│ ╔══[COMPILATION ERROR] Mutable field 'value' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.280: private class NestedCell(mut val value) +//│ ║ ^^^^^ +//│ ╟── This value is stored in this record field. +//│ ║ l.281: private fun original() = {item: 0} +//│ ║ ^^^^ +//│ ╟── This value is returned here. +//│ ║ l.284: let copy = mut {...intermediate()} +//│ ║ ^^^^^^^^^^^^^^^ +//│ ║ l.285: set copy.item = NestedCell(1) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.286: copy +//│ ║ ^^^^^^ +//│ ╟── 'exportNestedCopy' is exposed by this compilation unit. +//│ ║ l.283: fun exportNestedCopy() = +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ + + +// An assigned closure retains its generic factory's substitution and scope exits. +:ge +private fun make[T](seed: T) = + fun callback(value) = value.answer + let record = {mut callback: 0} + set record.callback = callback + callback({answer: seed}) + record +fun exportGenericCallback() = make(42) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.308: fun callback(value) = value.answer +//│ ║ ^^^^^^^^^^^^ +//│ ╟── Cannot resolve member 'answer' of a value with unknown shape. +//│ ╟── Parameter 'value' admits values of unknown shape. +//│ ║ l.308: fun callback(value) = value.answer +//│ ║ ^^^^^ +//│ ╟── This value is stored in this record field. +//│ ║ l.309: let record = {mut callback: 0} +//│ ║ ^^^^^^^^ +//│ ╟── This value is returned here. +//│ ║ l.313: fun exportGenericCallback() = make(42) +//│ ║ ^^^^^^^^ +//│ ╟── 'exportGenericCallback' is exposed by this compilation unit. +//│ ║ l.313: fun exportGenericCallback() = make(42) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +// Exposing a writer can publish assignments after the containing record was first visited. +:ge +private class LateCell(mut val value) +fun exportWriter() = + let record = {mut item: 0} + fun write() = set record.item = LateCell(1) + [record, write] +//│ ╔══[COMPILATION ERROR] Mutable field 'value' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.334: private class LateCell(mut val value) +//│ ║ ^^^^^ +//│ ╟── This value is stored in this record field. +//│ ║ l.336: let record = {mut item: 0} +//│ ║ ^^^^ +//│ ╟── This value is stored in this tuple. +//│ ║ l.338: [record, write] +//│ ║ ^^^^^^^^^^^^^^^ +//│ ╟── This value is returned here. +//│ ║ l.336: let record = {mut item: 0} +//│ ║ ^^^^^^^^ +//│ ║ l.337: fun write() = set record.item = LateCell(1) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.338: [record, write] +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── 'exportWriter' is exposed by this compilation unit. +//│ ║ l.335: fun exportWriter() = +//│ ╙── ^^^^^^^^^^^^^^^^^^ + + +// Exposing an immutable source does not expose values assigned only to its mutable copy. +:expect 0 +private class HiddenCell(mut val value) +fun exportSource() = + let source = {item: 0} + let copy = mut {...source} + set copy.item = HiddenCell(1) + source +exportSource().item +//│ = 0 + + +// An annotated generic field value exposes only its substituted interface, including through nested spreads. +// Separate factory activations must keep their callback domains distinct instead of exposing either callback +// to arbitrary inputs. +:expect [4, 6] +private fun make[T](callback: T -> Int) = + let record = {mut callback: 0} + set record.callback = callback + record +private fun length(value) = value.length +private fun increment(value) = value + 1 +fun exportStringCallback() = {...{...make[Str](length)}} +fun exportIntCallback() = {...make[Int](increment)} +[length("abcd"), increment(5)] +//│ = [4, 6] + + +// A later field replaces a spread field completely. Exposure must not follow the discarded value and require +// annotations on mutable class fields that remain private. +:expect 0 +private class OverwrittenCell(mut val value) +private val overwrittenSource = {hidden: OverwrittenCell(0)} +val overwrittenView = {...overwrittenSource, hidden: 0} +overwrittenView.hidden +//│ = 0 +//│ overwrittenSource = {hidden: OverwrittenCell(0)} +//│ overwrittenView = {hidden: 0} + + +// The same last-write-wins rule applies to repeated direct fields and fields replaced by a later spread. +:expect [0, 1] +private class ReplacedCell(mut val value) +fun exportDirectReplacement() = {item: ReplacedCell(0), item: 0} +fun exportSpreadReplacement() = {item: ReplacedCell(0), ...{item: 1}} +[exportDirectReplacement().item, exportSpreadReplacement().item] +//│ = [0, 1] + + +// Neither an initializer nor values assigned to its mutable field escape after a nested copy overwrites it. +:expect 0 +private class ReplacedAssignedCell(mut val value) +private fun originalWithAssignment() = + let record = {mut item: ReplacedAssignedCell(0)} + set record.item = ReplacedAssignedCell(1) + record +private fun intermediateWithAssignment() = {...originalWithAssignment()} +fun exportAssignedReplacement() = mut {...intermediateWithAssignment(), item: 0} +exportAssignedReplacement().item +//│ = 0 + + +// A computed key might leave earlier fields intact, so their values must still be exposed. +:ge +private class PossiblyRetainedCell(mut val value) +fun exportPossibleReplacement(key: Str) = {item: PossiblyRetainedCell(0), (key): 0} +//│ ╔══[COMPILATION ERROR] Mutable field 'value' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.424: private class PossiblyRetainedCell(mut val value) +//│ ║ ^^^^^ +//│ ╟── This value is stored in this record field. +//│ ║ l.425: fun exportPossibleReplacement(key: Str) = {item: PossiblyRetainedCell(0), (key): 0} +//│ ║ ^^^^ +//│ ╟── This value is returned here. +//│ ║ l.425: fun exportPossibleReplacement(key: Str) = {item: PossiblyRetainedCell(0), (key): 0} +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── 'exportPossibleReplacement' is exposed by this compilation unit. +//│ ║ l.425: fun exportPossibleReplacement(key: Str) = {item: PossiblyRetainedCell(0), (key): 0} +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +// A frozen spread copy does not let clients write to the private mutable record that it copied. +:expect 3 +private val privateMutableSource = {mut item: "abc"} +val exportedImmutableCopy = {...privateMutableSource} +privateMutableSource.item.length +//│ = 3 +//│ exportedImmutableCopy = {item: "abc"} +//│ privateMutableSource = {item: "abc"} + + +// Freezing the copy still exposes the nested values its source held when they were copied. +:ge +private class FrozenCopiedCell(mut val value) +fun exportFrozenCopy() = + let record = {mut item: 0} + set record.item = FrozenCopiedCell(1) + {...record} +//│ ╔══[COMPILATION ERROR] Mutable field 'value' is exposed to other compilation units, which can assign to it; annotate its type +//│ ║ l.452: private class FrozenCopiedCell(mut val value) +//│ ║ ^^^^^ +//│ ╟── This value is stored in this record field. +//│ ║ l.454: let record = {mut item: 0} +//│ ║ ^^^^ +//│ ╟── This value is returned here. +//│ ║ l.454: let record = {mut item: 0} +//│ ║ ^^^^^^^^ +//│ ║ l.455: set record.item = FrozenCopiedCell(1) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.456: {...record} +//│ ║ ^^^^^^^^^^^^ +//│ ╟── 'exportFrozenCopy' is exposed by this compilation unit. +//│ ║ l.453: fun exportFrozenCopy() = +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ + + +// An explicitly mutable copy still admits unknown writes through its copied fields. +:e +private val privateImmutableSource = {item: "abc"} +val exportedMutableCopy = mut {...privateImmutableSource} +exportedMutableCopy.item.length +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.479: exportedMutableCopy.item.length +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Cannot resolve member 'length' of a value with unknown shape. +//│ ╟── 'exportedMutableCopy' is exposed by this compilation unit. +//│ ║ l.478: val exportedMutableCopy = mut {...privateImmutableSource} +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. +//│ exportedMutableCopy = {item: "abc"} +//│ privateImmutableSource = {item: "abc"} diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalBinding.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalBinding.mls new file mode 100644 index 0000000000..ea33187b3a --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalBinding.mls @@ -0,0 +1,344 @@ +:. +open annotations + +// Missing optional arguments bind null. Unsafe calls below remain regression probes; +// their observed outputs do not define semantics for unsafe.undefined. +:sir +:sjs +:noInline +fun absent(x?) = x is null +[absent(), absent(unsafe.undefined), absent(null), absent(()), absent(0), absent(false)] +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp, tmp1, tmp2, tmp3, tmp4, tmp5; +//│ define absent⁰ as fun absent¹(x?) { +//│ match x +//│ null => +//│ return true +//│ else +//│ return false +//│ end +//│ }; +//│ set tmp = absent¹(null); +//│ set tmp1 = absent¹((undefined as!! Unknown)); +//│ set tmp2 = absent¹(null); +//│ set tmp3 = absent¹(runtime⁰.Unit⁰); +//│ set tmp4 = absent¹(0); +//│ set tmp5 = absent¹(false); +//│ return [tmp, tmp1, tmp2, tmp3, tmp4, tmp5] +//│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— +//│ let absent, tmp, tmp1, tmp2, tmp3, tmp4, tmp5; +//│ absent = function absent(x) { +//│ x ??= null; +//│ if (x === null) { +//│ return true +//│ } +//│ return false; +//│ }; +//│ tmp = absent(null); +//│ tmp1 = absent(undefined); +//│ tmp2 = absent(null); +//│ tmp3 = absent(runtime.Unit); +//│ tmp4 = absent(0); +//│ tmp5 = absent(false); +//│ return globalThis.Object.freeze([ tmp, tmp1, tmp2, tmp3, tmp4, tmp5 ]) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = [true, true, true, false, false, false] + +// Known omissions are explicit null arguments in the shared IR. +:sir +:noInline +:expect true +absent() +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ return absent¹(null) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = true + +// Optional defaults also apply through function values and lambdas. +:noInline +let callback = absent +let lambda = (x?) => x is null +[callback(), callback(unsafe.undefined), lambda(), lambda(unsafe.undefined)] +//│ = [true, true, true, true] +//│ callback = fun absent +//│ lambda = fun lambda + +// Each curried parameter list supplies its own defaults. +:noInline +fun curried(a, b, c)(x?) = x is null +[curried(1, 2, 3)(), curried(1, 2, 3)(unsafe.undefined), curried(1, 2, 3)(null), curried(1, 2, 3)(())] +//│ = [true, true, true, false] + +// Inlining binds omitted slots before substituting parameters into the body. +@inline private fun inlined(x?) = x is null +[inlined(), inlined(unsafe.undefined), inlined(null), inlined(())] +//│ = [true, false, true, false] + +// Fixed optional slots precede the rest tuple, including calls through spreads. +:noInline +fun withRest(x?, ...xs) = [x is null, xs.length] +fun fromSpread(xs: Array[Any]) = withRest(...xs) +[withRest(), withRest(unsafe.undefined), withRest(0, 1, 2), fromSpread([]), fromSpread([unsafe.undefined])] +//│ = [[true, 0], [true, 0], [false, 2], [true, 0], [true, 0]] + +// Optional slots must not consume required trailing arguments. +:noInline +fun trailing(x?, ...xs, last) = [x is null, xs.length, last] +fun trailingSpread(xs: Array[Any]) = trailing(...xs, 3) +[trailing(3), trailing(unsafe.undefined, 3), trailing(0, 2, 3), trailingSpread([]), trailingSpread([unsafe.undefined])] +//│ = [[true, 0, 3], [false, 0, 3], [false, 1, 3], [true, 0, 3], [false, 0, 3]] + +// Tail-call jumps bind null again when the next iteration omits an earlier value. +@tailrec fun omit(n, x?) = if n == 0 then x is null else omit(n - 1) +@tailrec fun keep(n, x?) = if n == 0 then x is null else keep(n - 1, x) +@tailrec fun spreadOmit(n, x?) = if n == 0 then x is null else spreadOmit(...[n - 1]) +@tailrec fun spreadKeep(n, x?) = if n == 0 then x is null else spreadKeep(...[n - 1, x]) +[omit(2, 1), keep(2, unsafe.undefined), spreadOmit(2, 1), spreadKeep(2, unsafe.undefined)] +//│ = [true, true, true, true] + +// Constructor flattening preserves empty slots between parameter lists. +class Fields(val first?)(val second?) { + constructor(val last) + fun values() = [first is null, second is null, last] +} +fun build(xs: Array[Any]) = new Fields(...xs)(2)(4) +[new Fields()(2)(4).values(), new Fields(unsafe.undefined)()(4).values(), build([]).values(), build([unsafe.undefined]).values()] +//│ = [[true, false, 4], [true, true, 4], [true, false, 4], [true, false, 4]] + +// Evaluate spread arguments once, then coerce the tuple's required field to its declared representation. +:noInline +:sir +:siret +:expect [true, false, 2] +class Box(val required: Int, val value?) +let count = 0 +fun makeArgs(present) = + set count = count + 1 + if present then [1, ()] else [1] +let missing = new Box(...makeArgs(false)) +let supplied = new Box(...makeArgs(true)) +[missing.value is null, supplied.value is null, count] +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let scrut: ?, scrut1: ?, tmp: Array⁰, constructorArgs: Array⁰, constructorArg: Int⁰, constructorArg1: Unknown, tmp1: Array⁰, constructorArgs1: Array⁰, constructorArg2: Int⁰, constructorArg3: Unknown, tmp2: Bool⁰, tmp3: Bool⁰; +//│ define makeArgs⁰ as fun makeArgs¹(present: Unknown): Array⁰ { +//│ let tmp4: Int⁰; +//│ set tmp4 = +⁰(count⁰, 1); +//│ set count⁰ = tmp4; +//│ match present +//│ true => +//│ return [1, runtime⁰.Unit⁰] +//│ else +//│ return [1] +//│ end +//│ }; +//│ define Box⁰ as class Box² { +//│ val required⁰: Int⁰; +//│ val value⁰: Unknown; +//│ constructor Box¹(required: Int⁰, value?: Unknown) { +//│ define required⁰ as val required¹: Int⁰ = required; +//│ define value⁰ as val value¹: Unknown = value; +//│ end +//│ } +//│ }; +//│ set count⁰ = 0; +//│ set tmp = makeArgs¹(false); +//│ set constructorArgs = [...tmp]; +//│ set constructorArg = (constructorArgs.0 as! Int⁰); +//│ match constructorArgs +//│ Array(2+) => +//│ set constructorArg1 = constructorArgs.1; +//│ end +//│ else +//│ set constructorArg1 = null; +//│ end +//│ set missing⁰ = new Box²(constructorArg, constructorArg1); +//│ set tmp1 = makeArgs¹(true); +//│ set constructorArgs1 = [...tmp1]; +//│ set constructorArg2 = (constructorArgs1.0 as! Int⁰); +//│ match constructorArgs1 +//│ Array(2+) => +//│ set constructorArg3 = constructorArgs1.1; +//│ end +//│ else +//│ set constructorArg3 = null; +//│ end +//│ set supplied⁰ = new Box²(constructorArg2, constructorArg3); +//│ set scrut = missing⁰.value¹; +//│ match scrut +//│ null => +//│ set tmp2 = true; +//│ end +//│ else +//│ set tmp2 = false; +//│ end +//│ set scrut1 = supplied⁰.value¹; +//│ match scrut1 +//│ null => +//│ set tmp3 = true; +//│ return [tmp2, tmp3, count⁰] +//│ else +//│ set tmp3 = false; +//│ return [tmp2, tmp3, count⁰] +//│ end +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = [true, false, 2] +//│ count = 2 +//│ missing = Box(1, null) +//│ supplied = Box(1, ()) + +// Eliminating constructors must still bind missing fields to null. +:lift +:deforest +:expect [true, false] +data class Data(value?) +private fun read(d: Data) = d.value is null +[read(Data()), read(Data(()))] +//│ deforest > >>> fusing >>> +//│ deforest > Data⁰(optionalArg⁰) -> +//│ deforest > select: value +//│ deforest > runtime⁰.Unit⁰ -> +//│ deforest > match: scrut⁰ +//│ deforest > Data⁰(null) -> +//│ deforest > select: value +//│ deforest > <<< fusing <<< +//│ = [true, false] + +// Defined values behave consistently across calls, inlining, spreads, and tail-call jumps. +:expect [true, true, false, false, false, true, true, true, true] +[absent(), absent(null), absent(()), absent(0), absent(false), inlined(), fromSpread([]).0, omit(2, 1), spreadOmit(2, 1)] +//│ = [true, true, false, false, false, true, true, true, true] + +// Optional trailing slots and constructor fields retain null when calls are rewritten. +:expect [[true, 0, 3], [true, 0, 3], true, [true, true, 4]] +[trailing(3), trailingSpread([]), read(Data()), new Fields()()(4).values()] +//│ = [[true, 0, 3], [true, 0, 3], true, [true, true, 4]] + +// Merged dispatchers reset omitted parameters on each transition. +:expect [true, true] +@tailrec fun even(n, x?) = if n == 0 then x is null else odd(n - 1) +@tailrec fun odd(n, x?) = if n == 0 then x is null else even(n - 1) +[even(3, 9), odd(3, 10)] +//│ = [true, true] + +// Closures retain each iteration's value, including a newly omitted slot. +:expect [null, 1, 2] +@tailrec fun captured(n, fs: Array[() -> Any], x?) = + if n == 0 then [(() => x), ...fs].map(f => f()) + else if n == 1 then captured(0, [(() => x), ...fs]) + else captured(n - 1, [(() => x), ...fs], n - 1) +captured(2, [], 2) +//│ = [null, 1, 2] + +// Scoped groups retain both captured values and optional defaults after inlining. +:expect [[10, null], [10, 11]] +let + offset = 10 + select(value?) = [offset, value] +in [select(), select(11)] +//│ = [[10, null], [10, 11]] + + +// Optional method arguments use the same binding rule as functions. +class Methods { + fun optional(x?) = x +} + +let methods = new Methods +[methods.optional(), methods.optional(unsafe.undefined), methods.optional(5)] +//│ = [null, null, 5] +//│ methods = Methods + + +// Unknown-length spreads may be empty even when their element type excludes null. +:e +private fun unguarded(x?, ...middle, last) = x.length +fun unknownSpread(xs: Array[Str]) = unguarded(...xs, "last") +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.254: private fun unguarded(x?, ...middle, last) = x.length +//│ ║ ^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.254: private fun unguarded(x?, ...middle, last) = x.length +//│ ╙── ^^ + +// Optionality alone admits null, even when every observed call supplies a string. +:e +private fun alwaysSupplied(x?) = x.length +alwaysSupplied("abc") +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.265: private fun alwaysSupplied(x?) = x.length +//│ ║ ^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.265: private fun alwaysSupplied(x?) = x.length +//│ ╙── ^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// Uncalled bodies have the same nullable parameter shape. +:e +private fun neverCalled(x?) = x.length +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.277: private fun neverCalled(x?) = x.length +//│ ║ ^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.277: private fun neverCalled(x?) = x.length +//│ ╙── ^^ + +// Callback constraints must not erase the optional parameter's null alternative. +:e +fun invoke(f: Str -> Int) = f("abc") +invoke((x?) => x.length) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.288: invoke((x?) => x.length) +//│ ║ ^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.288: invoke((x?) => x.length) +//│ ╙── ^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// Inferred constructor fields retain the optional parameter's null alternative too. +:e +private class NullableField(val value?) +NullableField("abc").value.length +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.300: NullableField("abc").value.length +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.299: private class NullableField(val value?) +//│ ╙── ^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// Dynamic construction keeps normalization when lowering cannot see the target class. +:lift +:deforest +data class Dynamic(value?) +private fun readDynamic(item: Dynamic) = item.value +let dynamicClass = Dynamic.class +[readDynamic(new! dynamicClass(unsafe.undefined)), readDynamic(new! dynamicClass(13))] +//│ deforest > >>> fusing >>> +//│ deforest > <<< fusing <<< +//│ = [null, 13] +//│ dynamicClass = class Dynamic + + +// Unscoped groups retain the complete optional, variadic, and trailing layout. +:expect [[1, null, [], 9], [1, 2, [3, 4], 5]] +let + collect(first, optional?, ...middle, last) = [first, optional, middle, last] +[collect(1, 9), collect(1, 2, 3, 4, 5)] +//│ = [[1, null, [], 9], [1, 2, [3, 4], 5]] +//│ collect = fun + +// The implicit receiver in `when` agrees with explicit `this`; `super` still +// resolves the parent method and passes optional arguments through its call. +class Parent { + open fun value(x?: Any): Any = x +} +class Child(val field?) extends Parent { + override fun value(x?) = when + Child as child then [child.field, this.field, super.value(x)] + else [] +} + +:expect [[null, null, null], [7, 7, 9], [7, 7, null]] +[(new Child()).value(), (new Child(7)).value(9), (new Child(7)).value(unsafe.undefined)] +//│ = [[null, null, null], [7, 7, 9], [7, 7, null]] diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalNull.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalNull.mls deleted file mode 100644 index f3785e1b2c..0000000000 --- a/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalNull.mls +++ /dev/null @@ -1,209 +0,0 @@ -:. -open annotations - - -// Optionality survives lowering, and normalization precedes every use in the body. -:sir -:sjs -:noInline -fun optional(x?) = x -//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define optional⁰ as fun optional¹(x?) { return x }; end -//│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let optional; optional = function optional(x) { x ??= null; return x }; -//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— - -[optional(), optional(unsafe.undefined), optional(null), optional(0), optional(false), optional("")] -//│ = [null, undefined, null, 0, false, ""] - -:sjs -:noInline -let optionalLambda = (x?) => x -//│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— -//│ let optionalLambda, optionalLambda1; -//│ optionalLambda1 = function optionalLambda(x) { -//│ x ??= null; -//│ return x -//│ }; -//│ optionalLambda = optionalLambda1; -//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ optionalLambda = fun optionalLambda - -[optionalLambda(), optionalLambda(unsafe.undefined), optionalLambda(1)] -//│ = [null, null, 1] - -// Inlining supplies null for omissions. Unsafe calls remain regression probes, -// without prescribing a particular result for unsafe.undefined. -@inline private fun inlineOptional(x?) = x -[inlineOptional(), inlineOptional(unsafe.undefined), inlineOptional(2)] -//│ = [null, undefined, 2] - -fun relay(x) = inlineOptional(x) -[relay(unsafe.undefined), relay(12)] -//│ = [undefined, 12] - -:noInline -fun curried(x?)(y?) = [x, y] -[curried()(), curried(unsafe.undefined)(3), curried(4)(unsafe.undefined)] -//│ = [[null, null], [null, 3], [4, null]] - -class Methods { - fun optional(x?) = x -} - -let methods = new Methods -[methods.optional(), methods.optional(unsafe.undefined), methods.optional(5)] -//│ = [null, null, 5] -//│ methods = Methods - -// A spread can omit a slot or explicitly supply undefined. -fun spread(xs: Array[Any]) = optional(...xs) -[spread([]), spread([unsafe.undefined]), spread([6])] -//│ = [null, null, 6] - -// Retain the unsafe flow through optional-parameter narrowing as a regression probe. -:re -private fun narrow(x?) = if x is ~null as x then x.length else 0 -[narrow(unsafe.undefined), narrow("hi")] -//│ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'length') - -// Unknown-length spreads may be empty even when their element type excludes null. -:e -private fun unguarded(x?, ...middle, last) = x.length -fun unknownSpread(xs: Array[Str]) = unguarded(...xs, "last") -//│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.72: private fun unguarded(x?, ...middle, last) = x.length -//│ ║ ^^^^^^^^ -//│ ╟── Null literal does not contain member 'length' -//│ ║ l.72: private fun unguarded(x?, ...middle, last) = x.length -//│ ╙── ^^ - -// Optionality alone admits null, even when every observed call supplies a string. -:e -private fun alwaysSupplied(x?) = x.length -alwaysSupplied("abc") -//│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.83: private fun alwaysSupplied(x?) = x.length -//│ ║ ^^^^^^^^ -//│ ╟── Null literal does not contain member 'length' -//│ ║ l.83: private fun alwaysSupplied(x?) = x.length -//│ ╙── ^^ -//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. - -// Uncalled bodies have the same nullable parameter shape. -:e -private fun neverCalled(x?) = x.length -//│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.95: private fun neverCalled(x?) = x.length -//│ ║ ^^^^^^^^ -//│ ╟── Null literal does not contain member 'length' -//│ ║ l.95: private fun neverCalled(x?) = x.length -//│ ╙── ^^ - -// Callback constraints must not erase the optional parameter's null alternative. -:e -fun invoke(f: Str -> Int) = f("abc") -invoke((x?) => x.length) -//│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.106: invoke((x?) => x.length) -//│ ║ ^^^^^^^^ -//│ ╟── Null literal does not contain member 'length' -//│ ║ l.106: invoke((x?) => x.length) -//│ ╙── ^^ -//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. - -// Inferred constructor fields retain the optional parameter's null alternative too. -:e -private class NullableField(val value?) -NullableField("abc").value.length -//│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.118: NullableField("abc").value.length -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -//│ ╟── Null literal does not contain member 'length' -//│ ║ l.117: private class NullableField(val value?) -//│ ╙── ^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. - - -// Tail-call jumps must reset omitted slots on every iteration, including spread calls. -:re -@tailrec fun loop(n, x?) = - if n == 0 then x else if n == 1 then loop(0, unsafe.undefined) else loop(n - 1, n) -@tailrec fun spreadLoop(n, x?) = if n == 0 then x else spreadLoop(...[n - 1]) -@tailrec fun spreadUndefined(n, x?) = if n == 0 then x else spreadUndefined(...[n - 1, unsafe.undefined]) -[loop(3), spreadLoop(3, 7), spreadUndefined(3, 8)] -//│ ═══[RUNTIME ERROR] Error: MLscript call unexpectedly returned `undefined`, the forbidden value. - -// Merged tail-call dispatchers have their own physical parameter list. -:re -@tailrec fun even(n, x?) = if n == 0 then x else odd(n - 1, unsafe.undefined) -@tailrec fun odd(n, x?) = if n == 0 then x else even(n - 1) -[even(3, 9), odd(3, 10)] -//│ ═══[RUNTIME ERROR] Error: MLscript call unexpectedly returned `undefined`, the forbidden value. - -// Captured parameters are copied per iteration and must already have their body values. -@tailrec fun captures(n, x?, ...fs: Array[() -> Any]) = - if n == 0 then [(() => x), ...fs].map(f => f()) - else if n == 1 then captures(0, unsafe.undefined, (() => x), ...fs) - else captures(n - 1, n - 1, (() => x), ...fs) -[captures(2, 2), captures(2)] -//│ = [[(), 1, 2], [(), 1, null]] - -// Constructor elimination must supply null for omitted optional fields. -:lift -:deforest -data class Data(value?) -private fun read(item) = item.value -[read(Data(unsafe.undefined)), read(Data()), read(Data(11))] -//│ deforest > >>> fusing >>> -//│ deforest > Data⁰(11) -> -//│ deforest > select: value -//│ deforest > Data⁰(null) -> -//│ deforest > select: value -//│ deforest > Data⁰(undefined) -> -//│ deforest > select: value -//│ deforest > <<< fusing <<< -//│ = [undefined, null, 11] - -// Dynamic construction keeps normalization when lowering cannot see the target class. -:lift -:deforest -data class Dynamic(value?) -private fun readDynamic(item: Dynamic) = item.value -let dynamicClass = Dynamic.class -[readDynamic(new! dynamicClass(unsafe.undefined)), readDynamic(new! dynamicClass(13))] -//│ deforest > >>> fusing >>> -//│ deforest > <<< fusing <<< -//│ = [null, 13] -//│ dynamicClass = class Dynamic - - -// Scoped binding groups preserve captures and normalize omitted optional arguments. -let - offset = 10 - select(value?) = [offset, value] -in [select(), select(11), select(unsafe.undefined)] -//│ = [[10, null], [10, 11], [10, undefined]] - -// Unscoped groups retain the complete optional, variadic, and trailing layout. -:expect [[1, null, [], 9], [1, 2, [3, 4], 5]] -let - collect(first, optional?, ...middle, last) = [first, optional, middle, last] -[collect(1, 9), collect(1, 2, 3, 4, 5)] -//│ = [[1, null, [], 9], [1, 2, [3, 4], 5]] -//│ collect = fun - -// The implicit receiver in `when` agrees with explicit `this`; `super` still -// resolves the parent method and passes optional arguments through its call. -class Parent { - open fun value(x?) = x -} -class Child(val field?) extends Parent { - override fun value(x?) = when - Child as child then [child.field, this.field, super.value(x)] - else [] -} - -:expect [[null, null, null], [7, 7, 9], [7, 7, null]] -[(new Child()).value(), (new Child(7)).value(9), (new Child(7)).value(unsafe.undefined)] -//│ = [[null, null, null], [7, 7, 9], [7, 7, null]] diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalParams.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalParams.mls index 33535d286f..ffae07a3c2 100644 --- a/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalParams.mls +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalParams.mls @@ -217,72 +217,30 @@ first(1, 2, 3) sum(3) //│ = 6 -// The omitted optional argument must be reset in each iteration. -:soir -:expect [[null], [null]] -@tailrec fun last(n, prev?) = - if n == 0 then [prev] else if n == 1 then last(n - 1) else last(n - 1, n) -[last(1), last(3)] -//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ let tmp, tmp1; -//│ @tailrec -//│ define last⁰ as fun last¹(n, prev?) { -//│ loop loopLabel: -//│ let scrut, scrut1, tmp2, tmp3; -//│ set scrut = Predef⁰.equals⁰(n, 0); -//│ match scrut -//│ true => -//│ return [prev] -//│ else -//│ set scrut1 = Predef⁰.equals⁰(n, 1); -//│ match scrut1 -//│ true => -//│ set tmp2 = -⁰(n, 1); -//│ set n = tmp2; -//│ set prev = null; -//│ continue loopLabel -//│ else -//│ let n_tmp; -//│ set tmp3 = -⁰(n, 1); -//│ set n_tmp = n; -//│ set n = tmp3; -//│ set prev = n_tmp; -//│ continue loopLabel -//│ end -//│ end -//│ unreachable /* Rest of abortive labelled block */ -//│ }; -//│ set tmp = last¹(1, null); -//│ set tmp1 = last¹(3, null); -//│ return [tmp, tmp1] -//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ = [[null], [null]] - - // *** Errors *** :e fun bad(x?, y) = x //│ ╔══[COMPILATION ERROR] A required parameter cannot follow an optional parameter. -//│ ║ l.265: fun bad(x?, y) = x +//│ ║ l.223: fun bad(x?, y) = x //│ ╙── ^ :e fun bad(...xs?) = xs //│ ╔══[COMPILATION ERROR] Variadic parameters cannot be optional. -//│ ║ l.271: fun bad(...xs?) = xs +//│ ║ l.229: fun bad(...xs?) = xs //│ ╙── ^^^^^^ :e fun bad(using x?: Int) = x //│ ╔══[COMPILATION ERROR] Contextual parameters cannot be optional. -//│ ║ l.277: fun bad(using x?: Int) = x +//│ ║ l.235: fun bad(using x?: Int) = x //│ ╙── ^^^^^^^^^^^^^ :e let t = [1, 2?] //│ ╔══[COMPILATION ERROR] Illegal position for an optional marker '?': only parameters and function type parameters can be optional. -//│ ║ l.283: let t = [1, 2?] +//│ ║ l.241: let t = [1, 2?] //│ ╙── ^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ t = undefined @@ -290,13 +248,13 @@ let t = [1, 2?] :e fun first(t: [Int, Int?]) = t.0 //│ ╔══[COMPILATION ERROR] Illegal position for an optional marker '?': only parameters and function type parameters can be optional. -//│ ║ l.291: fun first(t: [Int, Int?]) = t.0 +//│ ║ l.249: fun first(t: [Int, Int?]) = t.0 //│ ╙── ^^^^ :e fun app(f: (Int?, Int) -> Int) = f(1, 2) //│ ╔══[COMPILATION ERROR] A required parameter cannot follow an optional parameter. -//│ ║ l.297: fun app(f: (Int?, Int) -> Int) = f(1, 2) +//│ ║ l.255: fun app(f: (Int?, Int) -> Int) = f(1, 2) //│ ╙── ^^^ // Other operators starting with `?` are unaffected. @@ -314,28 +272,28 @@ unsafe.undefined ?: 1 :e fun bad(x, y ?: Int) = x //│ ╔══[COMPILATION ERROR] Expected a valid parameter, found operator application -//│ ║ l.315: fun bad(x, y ?: Int) = x +//│ ║ l.273: fun bad(x, y ?: Int) = x //│ ╙── ^^^^^^^^ :pe :e fun bad(x, y ?) = x //│ ╔══[PARSE ERROR] Expected start of expression in this position; found end of input instead -//│ ║ l.322: fun bad(x, y ?) = x +//│ ║ l.280: fun bad(x, y ?) = x //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Expected a valid parameter, found operator application -//│ ║ l.322: fun bad(x, y ?) = x +//│ ║ l.280: fun bad(x, y ?) = x //│ ╙── ^^^ :pe :e fun bad(f: (Int, Int ?) -> Int) = f(1) //│ ╔══[PARSE ERROR] Expected start of expression in this position; found end of input instead -//│ ║ l.332: fun bad(f: (Int, Int ?) -> Int) = f(1) +//│ ║ l.290: fun bad(f: (Int, Int ?) -> Int) = f(1) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.332: fun bad(f: (Int, Int ?) -> Int) = f(1) +//│ ║ l.290: fun bad(f: (Int, Int ?) -> Int) = f(1) //│ ║ ^^^^^ //│ ╟── This type has no resolved target -//│ ║ l.332: fun bad(f: (Int, Int ?) -> Int) = f(1) +//│ ║ l.290: fun bad(f: (Int, Int ?) -> Int) = f(1) //│ ╙── ^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/slop/wasm/MutableFields.mls b/hkmc2/shared/src/test/mlscript/slop/wasm/MutableFields.mls new file mode 100644 index 0000000000..a8fda78a25 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/wasm/MutableFields.mls @@ -0,0 +1,304 @@ +:. +#lang(strictResolution: true) +#lang(eagerResolution: true) +open wasm { i32 } +open i32 { const } +//│ Wasm result: +//│ = {} + + +// A mutable field without a declared type takes the representation of its initializer and of the +// values assigned to it. Here, all of them are unboxed `Int32`s, so the field stores an `i32`: +// storing them in a field of the unknown representation would require boxing them, which is an error. +:noInline +:expect 42 +private class Cell(mut val value) +private fun assign() = + let cell = Cell(const(1)) + set cell.value = const(42) + cell.value +assign() +//│ Wasm result: +//│ = 42 + +// Compound assignments are assignments too. On Wasm, the builtin operators are dynamically typed +// runtime functions, so their results have the unknown representation. +:ge +private class Counter(mut val count) +private fun increment() = + let counter = Counter(const(1)) + set counter.count += 1 + counter.count +increment() +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Unknown' have no common representation +//│ ║ l.30: set counter.count += 1 +//│ ╙── ^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Unknown' have no common representation +//│ ║ l.31: counter.count +//│ ╙── ^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Unknown' have no common representation +//│ ║ l.27: private class Counter(mut val count) +//│ ╙── ^^^^^ +//│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `i32` to `(ref null any)` in a different type hierarchy +//│ ═══[COMPILATION ERROR] Wasm entry function has incompatible result types `Int32` and `Unknown` + +// Assigning a value of an unrelated representation is rejected where the field is assigned and read. +:ge +private class Mixed(mut val value) +private fun mix() = + let mixed = Mixed(const(1)) + set mixed.value = "hello" + mixed.value +mix() +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.50: set mixed.value = "hello" +//│ ╙── ^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.51: mixed.value +//│ ╙── ^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.47: private class Mixed(mut val value) +//│ ╙── ^^^^^ +//│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `i32` to `(ref null any)` in a different type hierarchy +//│ ═══[COMPILATION ERROR] Wasm entry function has incompatible result types `Int32` and `Str` + +// Assignments in methods, through `this` or through the field's name, reach the field too. +:noInline +:expect 43 +private class Box with + mut val value = const(1) + fun put(v) = set this.value = v + fun reset() = set value = const(0) +private fun putInBox() = + let box = new Box + box.reset() + box.put(const(43)) + box.value +putInBox() +//│ Wasm result: +//│ = 43 + +// An assignment reaches the fields of every class its receiver can be an instance of, including +// inherited fields, whatever the class through which the receiver is passed. +:noInline +:expect 44 +private class Base(mut val value) +private class Derived(x) extends Base(x) +private fun setBase(base) = set base.value = const(44) +private fun inherit() = + let derived = Derived(const(1)) + setBase(derived) + derived.value +inherit() +//│ Wasm result: +//│ = 44 + +// Assignments through class projections reach the projected field. +:noInline +:expect 45 +private class Projected(mut val value) +private fun project() = + let projected = Projected(const(1)) + set projected.Projected#value = const(45) + projected.Projected#value +project() +//│ Wasm result: +//│ = 45 + + +// Assignments reach the field whatever the receiver's interface: here, a type annotation... +:ge +private class Typed(mut val value) +private fun setTyped(cell: Typed) = set cell.value = "hello" +private fun typed() = Typed(const(1)).value +typed() +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.112: private fun setTyped(cell: Typed) = set cell.value = "hello" +//│ ╙── ^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.113: private fun typed() = Typed(const(1)).value +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.111: private class Typed(mut val value) +//│ ╙── ^^^^^ +//│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `i32` to `(ref null any)` in a different type hierarchy +//│ ═══[COMPILATION ERROR] Wasm entry function has incompatible result types `Int32` and `Str` + +// ...and here, the class's own scope, from a nested definition. +:ge +private class Named with + mut val value = const(1) + fun rename() = set value = "renamed" +private fun named() = (new Named).value +named() +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.132: private fun named() = (new Named).value +//│ ╙── ^^^^^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.131: fun rename() = set value = "renamed" +//│ ╙── ^^^^^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.130: mut val value = const(1) +//│ ╙── ^^^^^^^^ +//│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `i32` to `(ref null any)` in a different type hierarchy +//│ ═══[COMPILATION ERROR] Wasm entry function has incompatible result types `Int32` and `Str` + +// ...and here, a wildcard open. +:ge +private class Opened(mut val value) +private fun openAndSet() = + let cell = Opened(const(1)) + open cell + set value = "opened" +openAndSet() +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.152: set value = "opened" +//│ ╙── ^^^^^ +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Str' have no common representation +//│ ║ l.148: private class Opened(mut val value) +//│ ╙── ^^^^^ +//│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `i32` to `(ref null any)` in a different type hierarchy + + +// Reading a mutable field returns its initializer and the values assigned to it. Here, `get` could +// otherwise be resolved statically to the method of `First` only, which would be called on the +// instance of `Second` assigned to the field. Strict resolution rejects the ambiguous selection. +:ge +private class First with + fun get() = const(1) +private class Second with + fun get() = const(2) +private class Holder(mut val item) +private fun replace() = + let holder = Holder(new First) + set holder.item = new Second + holder.item.get() +replace() +//│ ╔══[COMPILATION ERROR] This selection of member 'get' is ambiguous, as it has multiple resolved targets +//│ ║ l.175: holder.item.get() +//│ ║ ^^^^^^^^^^^^^^^ +//│ ╟── target: function 'get' in class 'Second' +//│ ║ l.170: fun get() = const(2) +//│ ║ ^^^ +//│ ╟── target: function 'get' in class 'First' +//│ ║ l.168: fun get() = const(1) +//│ ╙── ^^^ +//│ Wasm result: +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// Declaring the field's type gives the selection a single target, dispatched at run time. +:noInline +:expect 2 +private open class Getter with + open fun get(): Int32 = const(0) +private class FirstGetter extends Getter with + override fun get() = const(1) +private class SecondGetter extends Getter with + override fun get() = const(2) +private class GetterHolder(mut val item: Getter) +private fun replaceGetter() = + let holder = GetterHolder(new FirstGetter) + set holder.item = new SecondGetter + holder.item.get() +replaceGetter() +//│ Wasm result: +//│ = 2 + + +// An exposed mutable field can use an unboxed representation when its type is declared. +:noInline +:expect 2 +class Declared(mut val value: Int32) +private fun declared() = + let cell = Declared(const(1)) + set cell.value = const(2) + cell.value +declared() +//│ Wasm result: +//│ = 2 + +// Other compilation units cannot assign to a private field. +:noInline +:expect 3 +class Hidden with + private mut val value = const(1) + fun bump() = set value = const(3) + fun get() = value +private fun hidden() = + let cell = new Hidden + cell.bump() + cell.get() +hidden() +//│ Wasm result: +//│ = 3 + + +// Assignments in later blocks use the representation inferred in the field's own block. +:noInline +private class Later(mut val value) +private fun makeLater() = Later(const(1)) +//│ Wasm result: +//│ = {} + +// FIXME: Using a private class in a later Wasm worksheet block fails the cast to its struct at run time, +// whether its fields are mutable or not (as in `ReadLater` below). +:fixme +:noInline +:expect 46 +private fun assignLater() = + let later = makeLater() + set later.value = const(46) + later.value +assignLater() +//│ Wasm result: +//│ ═══[RUNTIME ERROR] RuntimeError: illegal cast + +:ge +private fun assignLaterString() = + let later = makeLater() + set later.value = "hello" +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int32' +//│ ║ l.259: set later.value = "hello" +//│ ╙── ^^^^^^^^^^^ +//│ Wasm result: +//│ = {} + +:noInline +private class ReadLater(val value) +private fun makeReadLater() = ReadLater(const(1)) +//│ Wasm result: +//│ = {} + +:fixme +:noInline +:expect 1 +makeReadLater().value +//│ Wasm result: +//│ ═══[RUNTIME ERROR] RuntimeError: illegal cast + + +// Through a record type declaring a mutable field, the values it describes can be assigned values of that field's +// type: here, values of the unknown representation, which an unboxed field cannot hold. +:ge +private class Described(mut val value) +private fun describe(described: {mut value: Any}) = () +private fun mutableView() = describe(Described(const(1))) +mutableView() +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Unknown' have no common representation +//│ ║ l.283: private class Described(mut val value) +//│ ╙── ^^^^^ +//│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `i32` to `(ref null any)` in a different type hierarchy + +// Fields that a record type does not declare `mut` are assumed not to be assigned through it: +// type checking will reject such assignments. +:noInline +:expect 1 +private class Viewed(mut val value) +private fun view(viewed: {value: Any}) = () +private fun immutableView() = + let viewed = Viewed(const(1)) + view(viewed) + viewed.value +immutableView() +//│ Wasm result: +//│ = 1 diff --git a/hkmc2/shared/src/test/mlscript/slop/wasm/OptionalParams.mls b/hkmc2/shared/src/test/mlscript/slop/wasm/OptionalParams.mls index c1cffa2090..024c6b381d 100644 --- a/hkmc2/shared/src/test/mlscript/slop/wasm/OptionalParams.mls +++ b/hkmc2/shared/src/test/mlscript/slop/wasm/OptionalParams.mls @@ -39,7 +39,7 @@ slots(null, null) :expect 1 slots(unsafe.undefined) //│ Wasm result: -//│ = 1 +//│ = 2 :expect 1 slots((), null) @@ -67,7 +67,9 @@ identity() :expect 11 val indirect: (Int, Int?) -> Anything = choose indirect(11) -//│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `(ref $choose)` to `(ref null any)` in a different type hierarchy +//│ ╔══[COMPILATION ERROR] Cannot cast a Wasm value of type `(ref $choose)` to `(ref null any)` in a different type hierarchy +//│ ║ l.68: val indirect: (Int, Int?) -> Anything = choose +//│ ╙── ^^^^^^ //│ ╔══[COMPILATION ERROR] Expected static function reference in Call(...) expression //│ ║ l.69: indirect(11) //│ ╙── ^^^^^^^^ @@ -77,7 +79,7 @@ indirect(11) fun forward(x) = absent(x) forward(unsafe.undefined) //│ Wasm result: -//│ = 0 +//│ = 1 :expect 1 forward(null) @@ -134,7 +136,7 @@ if Inherited().read() is 15 then Fields(1, 16).read() else -1 :e choose() //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.135: choose() +//│ ║ l.137: choose() //│ ║ ^^^^^^^^ //│ ╟── Function 'choose' expected 1 to 2 arguments, but got 0 //│ ║ l.8: fun choose(x: Int, y?: Int) = if y is null then x else y @@ -153,7 +155,7 @@ fun argument() = unsafe.undefined Normalize(argument()).read() + Normalize(argument()).read() * 10 + count * 5 //│ Wasm result: -//│ = 10 +//│ = 21 // Null and Unit remain distinct; keep the unsafe constructor call as a regression probe too. :expect 1 @@ -163,7 +165,7 @@ class OptionalField(val x?) { let uninitialized = OptionalField(unsafe.undefined).read() OptionalField(null).read() + OptionalField(()).read() * 10 + uninitialized * 100 //│ Wasm result: -//│ = 1 +//│ = 101 // TODO: A method call on a class from an earlier Wasm REPL block looks for a struct field. :fixme @@ -174,7 +176,7 @@ OptionalField(()).read() // A virtual call with a supplied optional argument preserves its value. :expect 17 class SuppliedBase() { - open fun pick(x?) = if x is null then 0 else x + open fun pick(x?: Int): Int = if x is null then 0 else x } class SuppliedChild() extends SuppliedBase() { override fun pick(x?) = if x is null then 0 else x @@ -186,7 +188,7 @@ suppliedDispatch(SuppliedChild(), 17) :expect 1112 class SuppliedBase() { - open fun pick(x?) = if x is null then 1 else x + open fun pick(x?: Int): Int = if x is null then 1 else x } class SuppliedChild() extends SuppliedBase() { override fun pick(x) = super.pick(x) @@ -230,6 +232,8 @@ keep(3, ()) // TODO: improve error :e fun test(x?: Int32) = x -//│ ═══[COMPILATION ERROR] Types 'Int32' and 'Unknown' have no common representation +//│ ╔══[COMPILATION ERROR] Types 'Int32' and 'Unknown' have no common representation +//│ ║ l.234: fun test(x?: Int32) = x +//│ ╙── ^ //│ Wasm result: //│ = {} diff --git a/hkmc2/shared/src/test/mlscript/slop/wasm/loose/.mls b/hkmc2/shared/src/test/mlscript/slop/wasm/loose/.mls new file mode 100644 index 0000000000..7e0f4b5167 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/wasm/loose/.mls @@ -0,0 +1,3 @@ +:.. +#lang(strictResolution: false) +#lang(eagerResolution: false) diff --git a/hkmc2/shared/src/test/mlscript/slop/wasm/loose/DeforestPrimitiveReturn.mls b/hkmc2/shared/src/test/mlscript/slop/wasm/loose/DeforestPrimitiveReturn.mls new file mode 100644 index 0000000000..eda86f0af2 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/wasm/loose/DeforestPrimitiveReturn.mls @@ -0,0 +1,47 @@ +:. +:sir +:siret +:deforest mono +:lift + +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ import "..." as Predef; end +// [Astra] Replacing a fused match with a call must preserve its unboxed result representation. +// [Astra] The generated call currently has an unknown result, which cannot be coerced to Int32. +:fixme +:expect 42 +data class Empty() +private fun f(n: Int32): Int32 = + let b = Empty() + if b is Empty then n else n +wasm.ref.i31(f(wasm.i32.const(42))) +//│ deforest > >>> fusing >>> +//│ deforest > Empty⁰() -> +//│ deforest > match: b⁰ +//│ deforest > <<< fusing <<< +//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Unknown' at an unrelated type 'Int32' +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp: Int32, tmp1: Int32, $b_Empty, $b_rest; +//│ @affine(1) @private +//│ define $b_Empty as fun $b_Empty⁰()(fv_n: Int32): ? { +//│ return fv_n +//│ }; +//│ @private +//│ define $b_rest as fun $b_rest⁰(): ? { +//│ return null +//│ }; +//│ @private +//│ define f⁰ as fun f¹(n: Int32): Int32 { +//│ let b: Function⁰; +//│ set b = $b_Empty⁰(); +//│ return b(n) +//│ }; +//│ @data +//│ define Empty¹ as class Empty² { +//│ constructor Empty⁰ +//│ }; +//│ set tmp = wasm⁰.i32⁰.const⁰(42); +//│ set tmp1 = f¹(tmp); +//│ return wasm⁰.ref⁰.i31⁰(tmp1) +//│ Wasm result: +//│ = 42 diff --git a/hkmc2/shared/src/test/mlscript/slop/wasm/loose/ErasureAcrossBlocks.mls b/hkmc2/shared/src/test/mlscript/slop/wasm/loose/ErasureAcrossBlocks.mls new file mode 100644 index 0000000000..2d3be5e49d --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/wasm/loose/ErasureAcrossBlocks.mls @@ -0,0 +1,32 @@ +:. + +// See `slop/loose/ErasureAcrossBlocks.mls`: without strict resolution, worksheet blocks infer the +// representations of their public definitions from their own uses only. + + +fun ident(x) = x +ident(1) +//│ Wasm result: +//│ = 1 + +:ge +ident("a") +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ║ l.13: ident("a") +//│ ╙── ^^^ +//│ Wasm result: +//│ = "a" + + +class Foo +class Box(val v) +let b = Box(new Foo) +//│ Wasm result: +//│ = {} + +// Reading a field whose representation is another session class crashes code generation: the session module +// does not import the types that the struct types it imports refer to. A declared field type (`val v: Foo`) +// fails the same way; inference makes such fields more common. +:fixme +b.v +//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: Missing type definition for type index `$Foo` diff --git a/hkmc2/shared/src/test/mlscript/slop/wasm/loose/FlowCastProofs.mls b/hkmc2/shared/src/test/mlscript/slop/wasm/loose/FlowCastProofs.mls new file mode 100644 index 0000000000..1600d11974 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/wasm/loose/FlowCastProofs.mls @@ -0,0 +1,67 @@ +:. +:global +:flowBasedOpt mono +:noInline + + +data class ProofBox(x: Int) +data class OtherBox(x: Int, y: Int) +//│ Wasm result: +//│ = {} + + +// A constructor that passes a cast cannot justify dropping a failing input to it. +// Both branches reach the same monomorphic cast, even though one is not executed. +:re +:soir +fun mixedCast(flag: Bool): Int = + fun check(x: Any): Int = + val unused: ProofBox = x + 42 + if flag then check(ProofBox(1)) else check(OtherBox(2, 3)) +mixedCast(false) +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ define mixedCast⁰ as fun mixedCast¹(flag) { +//│ let check, tmp, tmp1; +//│ define check as fun check⁰(x) { +//│ do (x as! ProofBox⁰); +//│ return 42 +//│ }; +//│ match flag +//│ true => +//│ set tmp = ProofBox¹(1); +//│ return check⁰(tmp) +//│ else +//│ set tmp1 = OtherBox⁰(2, 3); +//│ return check⁰(tmp1) +//│ end +//│ }; +//│ return mixedCast¹(false) +//│ Wasm result: +//│ ═══[RUNTIME ERROR] RuntimeError: illegal cast + + +// Unknown inputs invalidate the proof for the known constructor too. +:re +:soir +fun unknownCast(x: Any): Int = + fun check(y: Any): Int = + val unused: ProofBox = y + 42 + check(ProofBox(1)) + check(x) +unknownCast("not a box") +//│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— +//│ define unknownCast⁰ as fun unknownCast¹(x) { +//│ let check, tmp; +//│ define check as fun check¹(y) { +//│ do (y as! ProofBox⁰); +//│ return 42 +//│ }; +//│ set tmp = ProofBox¹(1); +//│ do check¹(tmp); +//│ return check¹(x) +//│ }; +//│ return unknownCast¹("not a box") +//│ Wasm result: +//│ ═══[RUNTIME ERROR] RuntimeError: illegal cast diff --git a/hkmc2/shared/src/test/mlscript/std/FingerTreeListTest.mls b/hkmc2/shared/src/test/mlscript/std/FingerTreeListTest.mls index 8815cff96a..cd86d345ff 100644 --- a/hkmc2/shared/src/test/mlscript/std/FingerTreeListTest.mls +++ b/hkmc2/shared/src/test/mlscript/std/FingerTreeListTest.mls @@ -102,14 +102,14 @@ if xs is //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let element0$; //│ if (runtime.Tuple.isArrayLike(xs2) && xs2.length >= 1) { -//│ let xs3, inlinedVal38, tmp16; +//│ let xs3, inlinedVal27, tmp16; //│ element0$ = runtime.Tuple.get(xs2, 0); //│ xs3 = xs2; //│ tmp16 = xs3.length - 0; -//│ inlinedVal38 = runtime.safeCall(xs3.slice(1, tmp16)); +//│ inlinedVal27 = runtime.safeCall(xs3.slice(1, tmp16)); //│ return globalThis.Object.freeze([ //│ element0$, -//│ inlinedVal38 +//│ inlinedVal27 //│ ]) //│ } //│ throw globalThis.Object.freeze(new globalThis.Error("match error")); diff --git a/hkmc2/shared/src/test/mlscript/std/PredefTest.mls b/hkmc2/shared/src/test/mlscript/std/PredefTest.mls index 3521cdbf98..62e1f9e4ec 100644 --- a/hkmc2/shared/src/test/mlscript/std/PredefTest.mls +++ b/hkmc2/shared/src/test/mlscript/std/PredefTest.mls @@ -300,7 +300,7 @@ x == y // data classes are equal from arguments :silent -data class A(a, b, c) +data class A(a: Any, b, c) module M with data class A(a, b, c) let x = A(1, 2, 3) diff --git a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls index 1f7a7ab9d2..c5f09e066d 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/TailRecOpt.mls @@ -50,7 +50,7 @@ f(10000, 20000, 0) //│ set id = 0; //│ continue loopLabel //│ else -//│ return param2 +//│ return (param2 as! Int⁰) //│ end //│ end //│ else @@ -170,7 +170,7 @@ fun g(n) = //│ set id = 1; //│ continue loopLabel //│ else -//│ return param5 +//│ return (param5 as! Array⁰) //│ end //│ 1 => //│ let tmp, tmp1, tmp2, argList, inlinedVal, tmp3; diff --git a/hkmc2/shared/src/test/mlscript/ucs/normalization/Deduplication.mls b/hkmc2/shared/src/test/mlscript/ucs/normalization/Deduplication.mls index 000fe1839c..92d413dea3 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/normalization/Deduplication.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/normalization/Deduplication.mls @@ -363,19 +363,19 @@ fun foo(x, y, z) = if x is //│ return "001" //│ else //│ set value = "hello"; -//│ return expensive_call⁰((value as! Str⁰)) +//│ return expensive_call⁰(value) //│ end //│ 1 => //│ return "01" //│ else //│ set value = "hello"; -//│ return expensive_call⁰((value as! Str⁰)) +//│ return expensive_call⁰(value) //│ end //│ 1 => //│ return "1" //│ else //│ set value = "hello"; -//│ return expensive_call⁰((value as! Str⁰)) +//│ return expensive_call⁰(value) //│ end //│ }; //│ end diff --git a/hkmc2/shared/src/test/mlscript/ucs/normalization/DeduplicationWhile.mls b/hkmc2/shared/src/test/mlscript/ucs/normalization/DeduplicationWhile.mls index 216670c0d3..1f256ecfa1 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/normalization/DeduplicationWhile.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/normalization/DeduplicationWhile.mls @@ -43,7 +43,7 @@ while true and true and false do print(1) else print(2) //│ set tmp = Predef⁰.print⁰(2); //│ break σ$x //│ continue lbl -//│ return runtime⁰.Unit⁰ +//│ end //│ }; //│ return lambda⁰ //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/ucs/patterns/RestTuple.mls b/hkmc2/shared/src/test/mlscript/ucs/patterns/RestTuple.mls index 8a8ca18e09..4bc2d5cd7a 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/patterns/RestTuple.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/patterns/RestTuple.mls @@ -10,9 +10,10 @@ fun nonsense(xs) = if xs is //│ let nonsense; //│ nonsense = function nonsense(xs) { //│ if (runtime.Tuple.isArrayLike(xs) && xs.length >= 0) { -//│ let tmp; +//│ let inlinedVal, tmp; //│ tmp = xs.length - 0; -//│ return runtime.safeCall(xs.slice(0, tmp)) +//│ inlinedVal = runtime.safeCall(xs.slice(0, tmp)); +//│ return inlinedVal //│ } //│ throw globalThis.Object.freeze(new globalThis.Error("match error")); //│ }; diff --git a/hkmc2/shared/src/test/mlscript/ucs/syntax/PlainConditionals.mls b/hkmc2/shared/src/test/mlscript/ucs/syntax/PlainConditionals.mls index f3c9cdb55e..51553c12fa 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/syntax/PlainConditionals.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/syntax/PlainConditionals.mls @@ -84,6 +84,10 @@ fun foo(x) = x is :e fun foo(x) = x is Pair(a, b) | Int +//│ ╔══[COMPILATION ERROR] Resolution error in alternation +//│ ║ l.86: fun foo(x) = x is Pair(a, b) | Int +//│ ║ ^^^^^^^^^^^^^^^^^^^^^ +//│ ╙── A type cannot be used as a runtime value. //│ ╔══[COMPILATION ERROR] Unexpected term form in expression position (alternation) //│ ║ l.86: fun foo(x) = x is Pair(a, b) | Int //│ ╙── ^^^^^^^^^^^^^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/ups/fixpoint/SimpleExample.mls b/hkmc2/shared/src/test/mlscript/ups/fixpoint/SimpleExample.mls index 2e75f84150..4cd02f5a76 100644 --- a/hkmc2/shared/src/test/mlscript/ups/fixpoint/SimpleExample.mls +++ b/hkmc2/shared/src/test/mlscript/ups/fixpoint/SimpleExample.mls @@ -43,14 +43,14 @@ pattern Steps = @compile (Step as Steps | _) //│ set tmp2 = { ...bindings, ...bindings1 }; //│ set a = tmp2.a﹖; //│ set b = tmp2.b﹖; -//│ set c = +⁰(a, b); +//│ set c = (+⁰(a, b) as! Int⁰); //│ set tmp3 = +⁰("reduce ", a); //│ set tmp4 = +⁰(tmp3, " + "); //│ set tmp5 = +⁰(tmp4, b); //│ set tmp6 = +⁰(tmp5, " = "); //│ set tmp7 = +⁰(tmp6, c); //│ do Predef⁰.print⁰(tmp7); -//│ set inlinedVal1 = Lit⁰((c as! Int⁰)); +//│ set inlinedVal1 = Lit⁰(c); //│ return new runtime⁰.MatchSuccess⁰(inlinedVal1, null) //│ else //│ return new runtime⁰.MatchFailure⁰("topmost") diff --git a/hkmc2/shared/src/test/mlscript/wasm/Basics.mls b/hkmc2/shared/src/test/mlscript/wasm/Basics.mls index 84f99670d2..0721e89b64 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/Basics.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/Basics.mls @@ -1,7 +1,5 @@ :. -:global -:wasm false @@ -34,7 +32,7 @@ x fun foo() = 42 foo //│ ╔══[COMPILATION ERROR] Returning function instances is not supported -//│ ║ l.35: foo +//│ ║ l.33: foo //│ ╙── ^^^ :ge @@ -42,7 +40,7 @@ class Foo with val x = 1 let f = Foo //│ ╔══[COMPILATION ERROR] Plain class references are not supported in Wasm; instantiate the class instead. -//│ ║ l.43: let f = Foo +//│ ║ l.41: let f = Foo //│ ╙── ^^^ @@ -59,11 +57,11 @@ foo() + foo() //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $foo (func (result (ref null any)))) +//│ (type $foo (func (result (ref null i31)))) //│ (type $plus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any)))) //│ (type $entry (func (result (ref null any)))) //│ (import "system" "plus_impl" (func $plus_impl (type $plus_impl))) -//│ (func $foo (export "foo") (type $foo) (result (ref null any)) +//│ (func $foo (export "foo") (type $foo) (result (ref null i31)) //│ (return //│ (ref.i31 //│ (i32.const 42)))) @@ -98,7 +96,7 @@ addOne(41) + useIt(41) //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) //│ (type $addOne (func (param $x (ref null i31)) (result (ref null any)))) -//│ (type $useIt (func (param $v (ref null any)) (result (ref null any)))) +//│ (type $useIt (func (param $v (ref null i31)) (result (ref null any)))) //│ (type $plus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any)))) //│ (type $entry (func (result (ref null any)))) //│ (import "system" "plus_impl" (func $plus_impl (type $plus_impl))) @@ -108,11 +106,10 @@ addOne(41) + useIt(41) //│ (local.get $x) //│ (ref.i31 //│ (i32.const 1))))) -//│ (func $useIt (export "useIt") (type $useIt) (param $v (ref null any)) (result (ref null any)) +//│ (func $useIt (export "useIt") (type $useIt) (param $v (ref null i31)) (result (ref null any)) //│ (return //│ (call $addOne -//│ (ref.cast (ref null i31) -//│ (local.get $v))))) +//│ (local.get $v)))) //│ (func $entry (export "entry") (type $entry) (result (ref null any)) //│ (local $tmp (ref null any)) //│ (local $tmp1 (ref null any)) @@ -149,15 +146,15 @@ new Foo(42).Foo#a //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $Foo (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $a (mut (ref null any)))))) +//│ (type $Foo (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $a (mut (ref null i31)))))) //│ (type $Foo_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $Foo_init (func (param $this (ref $Foo)) (param $a (ref null any)) (result (ref $Foo)))) -//│ (type $Foo_ctor (func (param $a (ref null any)) (result (ref $Foo)))) -//│ (type $entry (func (result (ref null any)))) +//│ (type $Foo_init (func (param $this (ref $Foo)) (param $a (ref null i31)) (result (ref $Foo)))) +//│ (type $Foo_ctor (func (param $a (ref null i31)) (result (ref $Foo)))) +//│ (type $entry (func (result (ref null i31)))) //│ (global $Foo_typeinfo (export "Foo_typeinfo") (ref $Foo_typeinfo) (struct.new $Foo_typeinfo //│ (i32.const 1) //│ (ref.null $TypeInfoBase))) -//│ (func $Foo_init (type $Foo_init) (param $this (ref $Foo)) (param $a (ref null any)) (result (ref $Foo)) +//│ (func $Foo_init (type $Foo_init) (param $this (ref $Foo)) (param $a (ref null i31)) (result (ref $Foo)) //│ (block (result (ref $Foo)) //│ (struct.set $Foo $a //│ (local.get $this) @@ -165,29 +162,30 @@ new Foo(42).Foo#a //│ (return //│ (ref.cast (ref $Foo) //│ (local.get $this))))) -//│ (func $Foo_ctor (export "Foo") (type $Foo_ctor) (param $a (ref null any)) (result (ref $Foo)) +//│ (func $Foo_ctor (export "Foo") (type $Foo_ctor) (param $a (ref null i31)) (result (ref $Foo)) //│ (local $this (ref $Foo)) //│ (block (result (ref $Foo)) //│ (local.set $this //│ (struct.new $Foo //│ (global.get $Foo_typeinfo) -//│ (ref.null any))) +//│ (ref.null i31))) //│ (drop //│ (call $Foo_init //│ (local.get $this) //│ (local.get $a))) //│ (return //│ (local.get $this)))) -//│ (func $entry (export "entry") (type $entry) (result (ref null any)) -//│ (local $tmp (ref $Foo)) -//│ (block (result (ref null any)) +//│ (func $entry (export "entry") (type $entry) (result (ref null i31)) +//│ (local $tmp (ref null $Foo)) +//│ (block (result (ref null i31)) //│ (local.set $tmp //│ (call $Foo_ctor //│ (ref.i31 //│ (i32.const 42)))) //│ (return //│ (struct.get $Foo $a -//│ (local.get $tmp))))) +//│ (ref.cast (ref $Foo) +//│ (local.get $tmp)))))) //│ (elem $Foo_init declare func $Foo_init) //│ (elem $Foo_ctor declare func $Foo_ctor) //│ (elem $entry declare func $entry)) @@ -212,15 +210,15 @@ getX(Foo(Bar(42))) //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $Bar (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any)))))) +//│ (type $Bar (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null i31)))))) //│ (type $Foo (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $bar (mut (ref null $Bar)))))) //│ (type $Bar_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $Bar_init (func (param $this (ref $Bar)) (param $x (ref null any)) (result (ref $Bar)))) -//│ (type $Bar_ctor (func (param $x (ref null any)) (result (ref $Bar)))) +//│ (type $Bar_init (func (param $this (ref $Bar)) (param $x (ref null i31)) (result (ref $Bar)))) +//│ (type $Bar_ctor (func (param $x (ref null i31)) (result (ref $Bar)))) //│ (type $Foo_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $Foo_init (func (param $this (ref $Foo)) (param $bar (ref $Bar)) (result (ref $Foo)))) -//│ (type $Foo_ctor (func (param $bar (ref $Bar)) (result (ref $Foo)))) -//│ (type $getX (func (param $f (ref $Foo)) (result (ref null any)))) +//│ (type $Foo_init (func (param $this (ref $Foo)) (param $bar (ref null $Bar)) (result (ref $Foo)))) +//│ (type $Foo_ctor (func (param $bar (ref null $Bar)) (result (ref $Foo)))) +//│ (type $getX (func (param $f (ref null $Foo)) (result (ref null any)))) //│ (type $entry (func (result (ref null any)))) //│ (global $Bar_typeinfo (export "Bar_typeinfo") (ref $Bar_typeinfo) (struct.new $Bar_typeinfo //│ (i32.const 1) @@ -228,7 +226,7 @@ getX(Foo(Bar(42))) //│ (global $Foo_typeinfo (export "Foo_typeinfo") (ref $Foo_typeinfo) (struct.new $Foo_typeinfo //│ (i32.const 2) //│ (ref.null $TypeInfoBase))) -//│ (func $Bar_init (type $Bar_init) (param $this (ref $Bar)) (param $x (ref null any)) (result (ref $Bar)) +//│ (func $Bar_init (type $Bar_init) (param $this (ref $Bar)) (param $x (ref null i31)) (result (ref $Bar)) //│ (block (result (ref $Bar)) //│ (struct.set $Bar $x //│ (local.get $this) @@ -236,20 +234,20 @@ getX(Foo(Bar(42))) //│ (return //│ (ref.cast (ref $Bar) //│ (local.get $this))))) -//│ (func $Bar_ctor (export "Bar") (type $Bar_ctor) (param $x (ref null any)) (result (ref $Bar)) +//│ (func $Bar_ctor (export "Bar") (type $Bar_ctor) (param $x (ref null i31)) (result (ref $Bar)) //│ (local $this (ref $Bar)) //│ (block (result (ref $Bar)) //│ (local.set $this //│ (struct.new $Bar //│ (global.get $Bar_typeinfo) -//│ (ref.null any))) +//│ (ref.null i31))) //│ (drop //│ (call $Bar_init //│ (local.get $this) //│ (local.get $x))) //│ (return //│ (local.get $this)))) -//│ (func $Foo_init (type $Foo_init) (param $this (ref $Foo)) (param $bar (ref $Bar)) (result (ref $Foo)) +//│ (func $Foo_init (type $Foo_init) (param $this (ref $Foo)) (param $bar (ref null $Bar)) (result (ref $Foo)) //│ (block (result (ref $Foo)) //│ (struct.set $Foo $bar //│ (local.get $this) @@ -257,7 +255,7 @@ getX(Foo(Bar(42))) //│ (return //│ (ref.cast (ref $Foo) //│ (local.get $this))))) -//│ (func $Foo_ctor (export "Foo") (type $Foo_ctor) (param $bar (ref $Bar)) (result (ref $Foo)) +//│ (func $Foo_ctor (export "Foo") (type $Foo_ctor) (param $bar (ref null $Bar)) (result (ref $Foo)) //│ (local $this (ref $Foo)) //│ (block (result (ref $Foo)) //│ (local.set $this @@ -270,15 +268,16 @@ getX(Foo(Bar(42))) //│ (local.get $bar))) //│ (return //│ (local.get $this)))) -//│ (func $getX (export "getX") (type $getX) (param $f (ref $Foo)) (result (ref null any)) +//│ (func $getX (export "getX") (type $getX) (param $f (ref null $Foo)) (result (ref null any)) //│ (return //│ (struct.get $Bar $x //│ (ref.cast (ref $Bar) //│ (struct.get $Foo $bar -//│ (local.get $f)))))) +//│ (ref.cast (ref $Foo) +//│ (local.get $f))))))) //│ (func $entry (export "entry") (type $entry) (result (ref null any)) -//│ (local $tmp (ref null any)) -//│ (local $tmp1 (ref null any)) +//│ (local $tmp (ref null $Bar)) +//│ (local $tmp1 (ref null $Foo)) //│ (block (result (ref null any)) //│ (local.set $tmp //│ (call $Bar_ctor @@ -286,12 +285,10 @@ getX(Foo(Bar(42))) //│ (i32.const 42)))) //│ (local.set $tmp1 //│ (call $Foo_ctor -//│ (ref.cast (ref $Bar) -//│ (local.get $tmp)))) +//│ (local.get $tmp))) //│ (return //│ (call $getX -//│ (ref.cast (ref $Foo) -//│ (local.get $tmp1)))))) +//│ (local.get $tmp1))))) //│ (elem $Bar_init declare func $Bar_init) //│ (elem $Bar_ctor declare func $Bar_ctor) //│ (elem $Foo_init declare func $Foo_init) @@ -320,15 +317,15 @@ new Foo(42).Foo#y //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $Foo (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any))) (field $y (mut (ref null any)))))) +//│ (type $Foo (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null i31))) (field $y (mut (ref null i31)))))) //│ (type $Foo_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $Foo_init (func (param $this (ref $Foo)) (param $x (ref null any)) (result (ref $Foo)))) -//│ (type $Foo_ctor (func (param $x (ref null any)) (result (ref $Foo)))) -//│ (type $entry (func (result (ref null any)))) +//│ (type $Foo_init (func (param $this (ref $Foo)) (param $x (ref null i31)) (result (ref $Foo)))) +//│ (type $Foo_ctor (func (param $x (ref null i31)) (result (ref $Foo)))) +//│ (type $entry (func (result (ref null i31)))) //│ (global $Foo_typeinfo (export "Foo_typeinfo") (ref $Foo_typeinfo) (struct.new $Foo_typeinfo //│ (i32.const 1) //│ (ref.null $TypeInfoBase))) -//│ (func $Foo_init (type $Foo_init) (param $this (ref $Foo)) (param $x (ref null any)) (result (ref $Foo)) +//│ (func $Foo_init (type $Foo_init) (param $this (ref $Foo)) (param $x (ref null i31)) (result (ref $Foo)) //│ (block (result (ref $Foo)) //│ (struct.set $Foo $x //│ (local.get $this) @@ -340,30 +337,31 @@ new Foo(42).Foo#y //│ (return //│ (ref.cast (ref $Foo) //│ (local.get $this))))) -//│ (func $Foo_ctor (export "Foo") (type $Foo_ctor) (param $x (ref null any)) (result (ref $Foo)) +//│ (func $Foo_ctor (export "Foo") (type $Foo_ctor) (param $x (ref null i31)) (result (ref $Foo)) //│ (local $this (ref $Foo)) //│ (block (result (ref $Foo)) //│ (local.set $this //│ (struct.new $Foo //│ (global.get $Foo_typeinfo) -//│ (ref.null any) -//│ (ref.null any))) +//│ (ref.null i31) +//│ (ref.null i31))) //│ (drop //│ (call $Foo_init //│ (local.get $this) //│ (local.get $x))) //│ (return //│ (local.get $this)))) -//│ (func $entry (export "entry") (type $entry) (result (ref null any)) -//│ (local $tmp (ref $Foo)) -//│ (block (result (ref null any)) +//│ (func $entry (export "entry") (type $entry) (result (ref null i31)) +//│ (local $tmp (ref null $Foo)) +//│ (block (result (ref null i31)) //│ (local.set $tmp //│ (call $Foo_ctor //│ (ref.i31 //│ (i32.const 42)))) //│ (return //│ (struct.get $Foo $y -//│ (local.get $tmp))))) +//│ (ref.cast (ref $Foo) +//│ (local.get $tmp)))))) //│ (elem $Foo_init declare func $Foo_init) //│ (elem $Foo_ctor declare func $Foo_ctor) //│ (elem $entry declare func $entry)) @@ -380,11 +378,11 @@ O.y //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $O (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any))) (field $y (mut (ref null any)))))) +//│ (type $O (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null i31))) (field $y (mut (ref null i31)))))) //│ (type $O_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $O_init (func (param $this (ref $O)) (result (ref $O)))) //│ (type $O_ctor (func (result (ref $O)))) -//│ (type $entry (func (result (ref null any)))) +//│ (type $entry (func (result (ref null i31)))) //│ (type $start (func)) //│ (global $O_typeinfo (export "O_typeinfo") (ref $O_typeinfo) (struct.new $O_typeinfo //│ (i32.const 1) @@ -409,8 +407,8 @@ O.y //│ (local.set $this //│ (struct.new $O //│ (global.get $O_typeinfo) -//│ (ref.null any) -//│ (ref.null any))) +//│ (ref.null i31) +//│ (ref.null i31))) //│ (drop //│ (call $O_init //│ (local.get $this))) @@ -419,7 +417,7 @@ O.y //│ (func $start (type $start) //│ (global.set $O$inst //│ (call $O_ctor))) -//│ (func $entry (export "entry") (type $entry) (result (ref null any)) +//│ (func $entry (export "entry") (type $entry) (result (ref null i31)) //│ (return //│ (struct.get $O $y //│ (ref.cast (ref $O) @@ -438,20 +436,17 @@ fun bar() = 42 fun foo() = bar foo()() //│ ╔══[COMPILATION ERROR] Returning function instances is not supported -//│ ║ l.438: fun foo() = bar +//│ ║ l.436: fun foo() = bar //│ ╙── ^^^ -// A primitive result has no `anyref` representation, so it cannot be widened and must be annotated. The IR -// rejects the widening; the backend reports it again, since a rejected value is left in place uncast. This is -// the unannotated-*result* case; `wasm/Casts.mls` covers the same rule for a value crossing an opaque call. +// The inferred result stays unboxed, but a boxed argument cannot be passed to an unboxed parameter. +// The IR rejects the argument; `wasm/Casts.mls` also checks values crossing an opaque call. :ge fun takeTop(m: Int32) = m takeTop(1) -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Int32' at an unrelated type 'Unknown' //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Int32' -//│ ║ l.450: takeTop(1) +//│ ║ l.447: takeTop(1) //│ ╙── ^ -//│ ╔══[COMPILATION ERROR] Wasm function `takeTop` expects result to have type `(ref null any)`, but got `i32` -//│ ║ l.449: fun takeTop(m: Int32) = m -//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ Wasm result: +//│ = 1 diff --git a/hkmc2/shared/src/test/mlscript/wasm/BuiltinOperators.mls b/hkmc2/shared/src/test/mlscript/wasm/BuiltinOperators.mls index 539959c6f6..c00db5d4a6 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/BuiltinOperators.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/BuiltinOperators.mls @@ -234,6 +234,16 @@ wasm.i32.eqz(wasm.i32.const(1)) //│ = 0 +// Chained trailing applications keep the unboxed representation of their intermediate results. +:expect 1 +42 + wasm.i32.const() + wasm.i32.eqz() + wasm.i32.eqz() +//│ Wasm result: +//│ = 1 + + // `2147483647` does not fit in an i31 and is truncated to all 31 bits set, which `get_s` reads as -1 and // `get_u` as 2147483647. wasm.i31.get_s(2147483647) @@ -403,3 +413,20 @@ not true not false //│ Wasm result: //│ = 1 + + +// Operators are implemented by dynamically typed runtime intrinsics, so the slots their results flow into +// keep the default reference representation, even when other inputs are integers (see `BuiltinOperatorResults`). +:noInline +:expect 43 +private fun pass(a) = a +let unused = pass(1) +pass(42 + 1) +//│ Wasm result: +//│ = 43 + + +:todo // String concatenation is not supported yet +"a" + 1 +//│ Wasm result: +//│ ═══[RUNTIME ERROR] RuntimeError: unreachable diff --git a/hkmc2/shared/src/test/mlscript/wasm/Casts.mls b/hkmc2/shared/src/test/mlscript/wasm/Casts.mls index adc377e509..3409905b2f 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/Casts.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/Casts.mls @@ -3,13 +3,13 @@ // In this example, the function call is equivalent to `useBox((id(new Box(42) as!! Box)))`. -// The `:wat` output below shows this as a `ref.cast` to `(ref $Box)` applied to the result of `id`, -// which is what the round-trip through an unannotated `id` makes necessary. +// The `:wat` output below shows this as a `ref.cast` to `(ref null $Box)` applied to the result of `id`, +// which is what the round-trip through an `Any`-typed `id` makes necessary. // Note that `:noInline` is needed here (and below) to prevent `id` from being inlined and the coercion elided. :noInline :wat class Box(val v: Int) -fun id(a) = a +fun id(a: Any): Any = a fun useBox(b: Box): Int = b.v useBox(id(new Box(42))) //│ Wat: @@ -21,7 +21,7 @@ useBox(id(new Box(42))) //│ (type $Box_init (func (param $this (ref $Box)) (param $v (ref null i31)) (result (ref $Box)))) //│ (type $Box_ctor (func (param $v (ref null i31)) (result (ref $Box)))) //│ (type $id (func (param $a (ref null any)) (result (ref null any)))) -//│ (type $useBox (func (param $b (ref $Box)) (result (ref null i31)))) +//│ (type $useBox (func (param $b (ref null $Box)) (result (ref null i31)))) //│ (type $entry (func (result (ref null i31)))) //│ (global $Box_typeinfo (export "Box_typeinfo") (ref $Box_typeinfo) (struct.new $Box_typeinfo //│ (i32.const 1) @@ -50,12 +50,13 @@ useBox(id(new Box(42))) //│ (func $id (export "id") (type $id) (param $a (ref null any)) (result (ref null any)) //│ (return //│ (local.get $a))) -//│ (func $useBox (export "useBox") (type $useBox) (param $b (ref $Box)) (result (ref null i31)) +//│ (func $useBox (export "useBox") (type $useBox) (param $b (ref null $Box)) (result (ref null i31)) //│ (return //│ (struct.get $Box $v -//│ (local.get $b)))) +//│ (ref.cast (ref $Box) +//│ (local.get $b))))) //│ (func $entry (export "entry") (type $entry) (result (ref null i31)) -//│ (local $tmp (ref $Box)) +//│ (local $tmp (ref null $Box)) //│ (local $tmp1 (ref null any)) //│ (block (result (ref null i31)) //│ (local.set $tmp @@ -67,7 +68,7 @@ useBox(id(new Box(42))) //│ (local.get $tmp))) //│ (return //│ (call $useBox -//│ (ref.cast (ref $Box) +//│ (ref.cast (ref null $Box) //│ (local.get $tmp1)))))) //│ (elem $Box_init declare func $Box_init) //│ (elem $Box_ctor declare func $Box_ctor) @@ -149,10 +150,10 @@ wasm.ref.i31(wasm.i32.const(7)) :noInline wasm.ref.i31(id(wasm.i32.const(7))) //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int32' at an unrelated type 'Unknown' -//│ ║ l.150: wasm.ref.i31(id(wasm.i32.const(7))) +//│ ║ l.151: wasm.ref.i31(id(wasm.i32.const(7))) //│ ╙── ^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Unknown' at an unrelated type 'Int32' -//│ ║ l.150: wasm.ref.i31(id(wasm.i32.const(7))) +//│ ║ l.151: wasm.ref.i31(id(wasm.i32.const(7))) //│ ╙── ^^^^^^^^^^^^^^^^^^^^^ //│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `i32` to `(ref null any)` in a different type hierarchy //│ ═══[COMPILATION ERROR] Operand #1 of wasm intrinsic 'ref.i31' must be of type 'Int32' @@ -177,7 +178,7 @@ unpack(toObj()) :e fun test: Function = id //│ ╔══[COMPILATION ERROR] Returning function instances is not supported -//│ ║ l.178: fun test: Function = id +//│ ║ l.179: fun test: Function = id //│ ╙── ^^ :fixme @@ -190,13 +191,17 @@ class C :e fun test: Function = new C -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'C' at an unrelated type 'Function' +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'C' at an unrelated type 'Function' +//│ ║ l.193: fun test: Function = new C +//│ ╙── ^^^^^ //│ Wasm result: //│ = {} :e fun test: Function = (new C) as Object -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'C' at an unrelated type 'Function' +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'C' at an unrelated type 'Function' +//│ ║ l.201: fun test: Function = (new C) as Object +//│ ╙── ^^^^^ //│ Wasm result: //│ = {} @@ -219,7 +224,7 @@ fun test: Function = castToA(123) :e castToA(wasm.i32.const(123)) //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int32' at an unrelated type 'Unknown' -//│ ║ l.220: castToA(wasm.i32.const(123)) +//│ ║ l.225: castToA(wasm.i32.const(123)) //│ ╙── ^^^^^^^^^^^^^^^^^^^ //│ Wasm result: //│ = 123 @@ -240,13 +245,13 @@ narrowUnitFromObject(()) //│ = {} :sir -fun narrowUnitFromUntyped(x): () = x -narrowUnitFromUntyped(()) +fun narrowUnitFromAny(x: Any): () = x +narrowUnitFromAny(()) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define narrowUnitFromUntyped⁰ as fun narrowUnitFromUntyped¹(x) { +//│ define narrowUnitFromAny⁰ as fun narrowUnitFromAny¹(x) { //│ return (x as! Unit⁰) //│ }; -//│ return narrowUnitFromUntyped¹(runtime⁰.Unit⁰) +//│ return narrowUnitFromAny¹(runtime⁰.Unit⁰) //│ Wasm result: //│ = {} @@ -273,7 +278,7 @@ narrowUnitInVal(()) :sir :soir :re -fun narrowLocal(x): Object = +fun narrowLocal(x: Any): Object = class Local() val y: Local = x y @@ -306,7 +311,7 @@ narrowLocal(42) :lift :soir :re -fun liftedNarrow(x): Object = +fun liftedNarrow(x: Any): Object = class Lifted() fun narrow(y): Lifted = y narrow(x) @@ -323,3 +328,23 @@ liftedNarrow(42) //│ return liftedNarrow¹(42) //│ Wasm result: //│ ═══[RUNTIME ERROR] RuntimeError: illegal cast + + +// Discarding a partial application must still execute its checked argument. +:noInline +:re +fun opaquePartialArg(x: Any): Any = x +fun discardPartial() = + fun checkedPartialArg(x: Box)(y: Int): Int = y + let unusedPartial = checkedPartialArg(opaquePartialArg("not a box")) + 42 +discardPartial() +//│ Wasm result: +//│ ═══[RUNTIME ERROR] RuntimeError: illegal cast + + +// An ascription narrows the dynamically typed result of an operator with a checked cast. +:expect 43 +42 + 1 as Int +//│ Wasm result: +//│ = 43 diff --git a/hkmc2/shared/src/test/mlscript/wasm/ClassInheritance.mls b/hkmc2/shared/src/test/mlscript/wasm/ClassInheritance.mls index 1a8b877b10..17c021cfeb 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/ClassInheritance.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/ClassInheritance.mls @@ -28,13 +28,13 @@ c.Parent#x + (c is Parent) //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) //│ (type $Parent (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any)))))) -//│ (type $Child (sub $Parent (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any))) (field $y (mut (ref null any)))))) +//│ (type $Child (sub $Parent (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any))) (field $y (mut (ref null i31)))))) //│ (type $Parent_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Parent_init (func (param $this (ref $Parent)) (param $x (ref null any)) (result (ref $Parent)))) //│ (type $Parent_ctor (func (param $x (ref null any)) (result (ref $Parent)))) //│ (type $Child_typeinfo (sub $Parent_typeinfo (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $Child_init (func (param $this (ref $Child)) (param $y (ref null any)) (result (ref $Child)))) -//│ (type $Child_ctor (func (param $y (ref null any)) (result (ref $Child)))) +//│ (type $Child_init (func (param $this (ref $Child)) (param $y (ref null i31)) (result (ref $Child)))) +//│ (type $Child_ctor (func (param $y (ref null i31)) (result (ref $Child)))) //│ (type $plus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any)))) //│ (type $entry (func (result (ref null any)))) //│ (import "system" "plus_impl" (func $plus_impl (type $plus_impl))) @@ -66,7 +66,7 @@ c.Parent#x + (c is Parent) //│ (local.get $x))) //│ (return //│ (local.get $this)))) -//│ (func $Child_init (type $Child_init) (param $this (ref $Child)) (param $y (ref null any)) (result (ref $Child)) +//│ (func $Child_init (type $Child_init) (param $this (ref $Child)) (param $y (ref null i31)) (result (ref $Child)) //│ (local $tmp (ref null any)) //│ (block (result (ref $Child)) //│ (local.set $tmp @@ -84,14 +84,14 @@ c.Parent#x + (c is Parent) //│ (return //│ (ref.cast (ref $Child) //│ (local.get $this))))) -//│ (func $Child_ctor (export "Child") (type $Child_ctor) (param $y (ref null any)) (result (ref $Child)) +//│ (func $Child_ctor (export "Child") (type $Child_ctor) (param $y (ref null i31)) (result (ref $Child)) //│ (local $this (ref $Child)) //│ (block (result (ref $Child)) //│ (local.set $this //│ (struct.new $Child //│ (global.get $Child_typeinfo) //│ (ref.null any) -//│ (ref.null any))) +//│ (ref.null i31))) //│ (drop //│ (call $Child_init //│ (local.get $this) @@ -113,14 +113,16 @@ c.Parent#x + (c is Parent) //│ (ref.null any)) //│ (if //│ (ref.test (ref $Object) -//│ (global.get $c)) +//│ (ref.cast (ref null $Child) +//│ (global.get $c))) //│ (then //│ (if //│ (block (result i32) //│ (local.set $currentTypeInfo //│ (struct.get $Object $$typeinfo //│ (ref.cast (ref $Object) -//│ (global.get $c)))) +//│ (ref.cast (ref null $Child) +//│ (global.get $c))))) //│ (local.set $targetTypeInfo //│ (global.get $Parent_typeinfo)) //│ (local.set $typeInfoMatch @@ -159,14 +161,16 @@ c.Parent#x + (c is Parent) //│ (call $plus_impl //│ (struct.get $Parent $x //│ (ref.cast (ref $Parent) -//│ (global.get $c))) +//│ (ref.cast (ref null $Child) +//│ (global.get $c)))) //│ (ref.i31 //│ (i32.const 1))))))))) //│ (return //│ (call $plus_impl //│ (struct.get $Parent $x //│ (ref.cast (ref $Parent) -//│ (global.get $c))) +//│ (ref.cast (ref null $Child) +//│ (global.get $c)))) //│ (ref.i31 //│ (i32.const 0))))) //│ (unreachable))) @@ -266,10 +270,10 @@ class B class A extends B class B extends A //│ ╔══[COMPILATION ERROR] Inheritance cycle involving 'A' -//│ ║ l.266: class A extends B +//│ ║ l.270: class A extends B //│ ╙── ^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Inheritance cycles are not supported. -//│ ║ l.266: class A extends B +//│ ║ l.270: class A extends B //│ ╙── ^^^^^^^^^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/wasm/ClassMethods.mls b/hkmc2/shared/src/test/mlscript/wasm/ClassMethods.mls index b563b84d8a..019189f341 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/ClassMethods.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/ClassMethods.mls @@ -11,11 +11,11 @@ a.A#get() + A(2).get() //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $A (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any)))))) +//│ (type $A (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null i31)))))) //│ (type $A_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $A_init (func (param $this (ref $A)) (param $x (ref null any)) (result (ref $A)))) -//│ (type $A_ctor (func (param $x (ref null any)) (result (ref $A)))) -//│ (type $A_get (func (param $this (ref $A)) (result (ref null any)))) +//│ (type $A_init (func (param $this (ref $A)) (param $x (ref null i31)) (result (ref $A)))) +//│ (type $A_ctor (func (param $x (ref null i31)) (result (ref $A)))) +//│ (type $A_get (func (param $this (ref $A)) (result (ref null i31)))) //│ (type $plus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any)))) //│ (type $entry (func (result (ref null any)))) //│ (import "system" "plus_impl" (func $plus_impl (type $plus_impl))) @@ -23,7 +23,7 @@ a.A#get() + A(2).get() //│ (global $A_typeinfo (export "A_typeinfo") (ref $A_typeinfo) (struct.new $A_typeinfo //│ (i32.const 1) //│ (ref.null $TypeInfoBase))) -//│ (func $A_init (type $A_init) (param $this (ref $A)) (param $x (ref null any)) (result (ref $A)) +//│ (func $A_init (type $A_init) (param $this (ref $A)) (param $x (ref null i31)) (result (ref $A)) //│ (block (result (ref $A)) //│ (struct.set $A $x //│ (local.get $this) @@ -31,27 +31,27 @@ a.A#get() + A(2).get() //│ (return //│ (ref.cast (ref $A) //│ (local.get $this))))) -//│ (func $A_ctor (export "A") (type $A_ctor) (param $x (ref null any)) (result (ref $A)) +//│ (func $A_ctor (export "A") (type $A_ctor) (param $x (ref null i31)) (result (ref $A)) //│ (local $this (ref $A)) //│ (block (result (ref $A)) //│ (local.set $this //│ (struct.new $A //│ (global.get $A_typeinfo) -//│ (ref.null any))) +//│ (ref.null i31))) //│ (drop //│ (call $A_init //│ (local.get $this) //│ (local.get $x))) //│ (return //│ (local.get $this)))) -//│ (func $A_get (type $A_get) (param $this (ref $A)) (result (ref null any)) +//│ (func $A_get (type $A_get) (param $this (ref $A)) (result (ref null i31)) //│ (return //│ (struct.get $A $x //│ (local.get $this)))) //│ (func $entry (export "entry") (type $entry) (result (ref null any)) -//│ (local $tmp (ref null any)) -//│ (local $tmp1 (ref null any)) -//│ (local $tmp2 (ref null any)) +//│ (local $tmp (ref null i31)) +//│ (local $tmp1 (ref null $A)) +//│ (local $tmp2 (ref null i31)) //│ (block (result (ref null any)) //│ (global.set $a //│ (call $A_ctor @@ -60,7 +60,8 @@ a.A#get() + A(2).get() //│ (local.set $tmp //│ (call $A_get //│ (ref.cast (ref $A) -//│ (global.get $a)))) +//│ (ref.cast (ref null $A) +//│ (global.get $a))))) //│ (local.set $tmp1 //│ (call $A_ctor //│ (ref.i31 @@ -175,18 +176,18 @@ c.Counter#inc().Counter#double().Counter#get() :todo let d = Counter(c.Counter#get()) -//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: Missing field `term:Counter/get` in struct `member:Counter` with type `(type $Counter (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any))))))` +//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: Missing field `term:Counter/get` in struct `member:Counter` with type `(type $Counter (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null i31))))))` :todo d.Counter#inc().Counter#double().Counter#get() //│ ╔══[COMPILATION ERROR] Cannot find variable `d` (VarSymbol) in local or global scope. -//│ ║ l.177: let d = Counter(c.Counter#get()) +//│ ║ l.178: let d = Counter(c.Counter#get()) //│ ╙── ^ -//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: Missing field `term:Counter/inc` in struct `member:Counter` with type `(type $Counter (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any))))))` +//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: Missing field `term:Counter/inc` in struct `member:Counter` with type `(type $Counter (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null i31))))))` :todo c.Counter#get() -//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: Missing field `term:Counter/get` in struct `member:Counter` with type `(type $Counter (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any))))))` +//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: Missing field `term:Counter/get` in struct `member:Counter` with type `(type $Counter (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null i31))))))` class GetterOnly() with @@ -201,7 +202,7 @@ class A(val x) with fun get() = x A(1).get //│ ╔══[COMPILATION ERROR] `member:get` is neither a field access nor a callable method -//│ ║ l.202: A(1).get +//│ ║ l.203: A(1).get //│ ╙── ^^^^^^^^ @@ -215,17 +216,17 @@ Prim(0).take(n32) //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $Prim (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $v (mut (ref null any)))))) +//│ (type $Prim (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $v (mut (ref null i31)))))) //│ (type $Prim_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $Prim_init (func (param $this (ref $Prim)) (param $v (ref null any)) (result (ref $Prim)))) -//│ (type $Prim_ctor (func (param $v (ref null any)) (result (ref $Prim)))) +//│ (type $Prim_init (func (param $this (ref $Prim)) (param $v (ref null i31)) (result (ref $Prim)))) +//│ (type $Prim_ctor (func (param $v (ref null i31)) (result (ref $Prim)))) //│ (type $Prim_take (func (param $this (ref $Prim)) (param $m i32) (result i32))) //│ (type $entry (func (result i32))) //│ (global $n32 (export "n32") (mut i32) (i32.const 0)) //│ (global $Prim_typeinfo (export "Prim_typeinfo") (ref $Prim_typeinfo) (struct.new $Prim_typeinfo //│ (i32.const 1) //│ (ref.null $TypeInfoBase))) -//│ (func $Prim_init (type $Prim_init) (param $this (ref $Prim)) (param $v (ref null any)) (result (ref $Prim)) +//│ (func $Prim_init (type $Prim_init) (param $this (ref $Prim)) (param $v (ref null i31)) (result (ref $Prim)) //│ (block (result (ref $Prim)) //│ (struct.set $Prim $v //│ (local.get $this) @@ -233,13 +234,13 @@ Prim(0).take(n32) //│ (return //│ (ref.cast (ref $Prim) //│ (local.get $this))))) -//│ (func $Prim_ctor (export "Prim") (type $Prim_ctor) (param $v (ref null any)) (result (ref $Prim)) +//│ (func $Prim_ctor (export "Prim") (type $Prim_ctor) (param $v (ref null i31)) (result (ref $Prim)) //│ (local $this (ref $Prim)) //│ (block (result (ref $Prim)) //│ (local.set $this //│ (struct.new $Prim //│ (global.get $Prim_typeinfo) -//│ (ref.null any))) +//│ (ref.null i31))) //│ (drop //│ (call $Prim_init //│ (local.get $this) @@ -251,7 +252,7 @@ Prim(0).take(n32) //│ (local.get $m))) //│ (func $entry (export "entry") (type $entry) (result i32) //│ (local $tmp i32) -//│ (local $tmp1 (ref null any)) +//│ (local $tmp1 (ref null $Prim)) //│ (block (result i32) //│ (local.set $tmp //│ (i32.const 7)) diff --git a/hkmc2/shared/src/test/mlscript/wasm/ControlFlow.mls b/hkmc2/shared/src/test/mlscript/wasm/ControlFlow.mls index 468cf8a296..4390199ba4 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/ControlFlow.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/ControlFlow.mls @@ -143,9 +143,9 @@ tailMatchAllValue(1) //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $tailMatchAllValue (func (param $x (ref null any)) (result (ref null any)))) +//│ (type $tailMatchAllValue (func (param $x (ref null i31)) (result (ref null i31)))) //│ (type $entry (func (result (ref null i31)))) -//│ (func $tailMatchAllValue (export "tailMatchAllValue") (type $tailMatchAllValue) (param $x (ref null any)) (result (ref null any)) +//│ (func $tailMatchAllValue (export "tailMatchAllValue") (type $tailMatchAllValue) (param $x (ref null i31)) (result (ref null i31)) //│ (local $matchRes (ref null any)) //│ (block //│ (block $match diff --git a/hkmc2/shared/src/test/mlscript/wasm/DeadConstructorElim.mls b/hkmc2/shared/src/test/mlscript/wasm/DeadConstructorElim.mls index a0a8c04f15..079ed1f0b3 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/DeadConstructorElim.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/DeadConstructorElim.mls @@ -1,8 +1,6 @@ :. :noInline -:global -:wasm data class Box(x) @@ -10,7 +8,7 @@ data class Box(x) //│ = {} -// a cast observes its operand even when the cast's result is unused, so the allocation must remain live +// A cast proved to succeed does not keep an otherwise-unused allocation live. :flowBasedOpt mono :soir fun cast() = @@ -19,7 +17,7 @@ fun cast() = 42 cast() //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ define cast⁰ as fun cast¹() { let x; set x = Box⁰(1); do (x as! Box¹); return 42 }; return cast¹() +//│ define cast⁰ as fun cast¹() { return 42 }; return cast¹() //│ Wasm result: //│ = 42 @@ -32,7 +30,7 @@ fun cast() = 42 cast() //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— -//│ define cast² as fun cast³() { let x; set x = Box⁰(1); do (x as! Box¹); return 42 }; return cast³() +//│ define cast² as fun cast³() { do (undefined as! Box⁰); return 42 }; return cast³() //│ Wasm result: //│ = 42 @@ -48,7 +46,7 @@ f() //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define f⁰ as fun f¹() { //│ let mk; -//│ define mk as fun mk⁰() { return (Box⁰(1) as! Box¹) }; +//│ define mk as fun mk⁰() { return undefined }; //│ do mk⁰(); //│ return 42 //│ }; @@ -58,8 +56,7 @@ f() -// although dead param elim removes the param, the checked cast output due to -// the call to `take` still consumes the `Box(1)` so the allocation is not removed +// A known identity call also permits removal of its unused argument and allocation. :flowBasedOpt mono :soir fun f() = @@ -69,12 +66,8 @@ fun f() = f() //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ define f² as fun f³() { -//│ let take, tmp; -//│ define take as fun take⁰() { -//│ return undefined -//│ }; -//│ set tmp = Box⁰(1); -//│ do (tmp as! Box¹); +//│ let take; +//│ define take as fun take⁰() { return undefined }; //│ do take⁰(); //│ return 42 //│ }; diff --git a/hkmc2/shared/src/test/mlscript/wasm/DeadParamElim.mls b/hkmc2/shared/src/test/mlscript/wasm/DeadParamElim.mls index 1513f84d04..44bd5a8cf2 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/DeadParamElim.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/DeadParamElim.mls @@ -1,8 +1,6 @@ :. :noInline -:global -:wasm data class Box(x) @@ -14,7 +12,7 @@ data class Box(x) :re :flowBasedOpt mono :soir -fun f(o) = +fun f(o: Any) = fun take(b: Box) = 0 take(o) f(0) @@ -28,3 +26,5 @@ f(0) //│ return f¹(0) //│ Wasm result: //│ ═══[RUNTIME ERROR] RuntimeError: illegal cast + + diff --git a/hkmc2/shared/src/test/mlscript/wasm/MainFunctions.mls b/hkmc2/shared/src/test/mlscript/wasm/MainFunctions.mls index 5b5c95fa58..85dce68583 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/MainFunctions.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/MainFunctions.mls @@ -34,7 +34,7 @@ throw x //│ (type $mlx_exn (func (param $ex (ref null any)))) //│ (type $entry (func (result (ref null any)))) //│ (tag $mlx_exn (export "mlx_exn") (type $mlx_exn)) -//│ (global $x (export "x") (mut (ref null any)) (ref.null any)) +//│ (global $x (export "x") (mut (ref null i31)) (ref.null i31)) //│ (func $entry (export "entry") (type $entry) (result (ref null any)) //│ (block //│ (global.set $x diff --git a/hkmc2/shared/src/test/mlscript/wasm/Matching.mls b/hkmc2/shared/src/test/mlscript/wasm/Matching.mls index b1b366209d..0b8e5b0e06 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/Matching.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/Matching.mls @@ -44,11 +44,11 @@ if Bar(true) is //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $Bar (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $y (mut (ref null any)))))) +//│ (type $Bar (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $y (mut (ref null i31)))))) //│ (type $Baz (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $z (mut (ref null any)))))) //│ (type $Bar_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $Bar_init (func (param $this (ref $Bar)) (param $y (ref null any)) (result (ref $Bar)))) -//│ (type $Bar_ctor (func (param $y (ref null any)) (result (ref $Bar)))) +//│ (type $Bar_init (func (param $this (ref $Bar)) (param $y (ref null i31)) (result (ref $Bar)))) +//│ (type $Bar_ctor (func (param $y (ref null i31)) (result (ref $Bar)))) //│ (type $Baz_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Baz_init (func (param $this (ref $Baz)) (param $z (ref null any)) (result (ref $Baz)))) //│ (type $Baz_ctor (func (param $z (ref null any)) (result (ref $Baz)))) @@ -59,7 +59,7 @@ if Bar(true) is //│ (global $Baz_typeinfo (export "Baz_typeinfo") (ref $Baz_typeinfo) (struct.new $Baz_typeinfo //│ (i32.const 2) //│ (ref.null $TypeInfoBase))) -//│ (func $Bar_init (type $Bar_init) (param $this (ref $Bar)) (param $y (ref null any)) (result (ref $Bar)) +//│ (func $Bar_init (type $Bar_init) (param $this (ref $Bar)) (param $y (ref null i31)) (result (ref $Bar)) //│ (block (result (ref $Bar)) //│ (struct.set $Bar $y //│ (local.get $this) @@ -67,13 +67,13 @@ if Bar(true) is //│ (return //│ (ref.cast (ref $Bar) //│ (local.get $this))))) -//│ (func $Bar_ctor (export "Bar") (type $Bar_ctor) (param $y (ref null any)) (result (ref $Bar)) +//│ (func $Bar_ctor (export "Bar") (type $Bar_ctor) (param $y (ref null i31)) (result (ref $Bar)) //│ (local $this (ref $Bar)) //│ (block (result (ref $Bar)) //│ (local.set $this //│ (struct.new $Bar //│ (global.get $Bar_typeinfo) -//│ (ref.null any))) +//│ (ref.null i31))) //│ (drop //│ (call $Bar_init //│ (local.get $this) @@ -275,11 +275,11 @@ getXor(Foo(42)) // * Note: when implemented, the resolution pass will reject matches on `Unknown`, removing this runtime type confusion :sir :siret -fun isInt(v) = if v is Int then 1 else 0 -fun isBool(v) = if v is Bool then 1 else 0 +fun isInt(v: Any) = if v is Int then 1 else 0 +fun isBool(v: Any) = if v is Bool then 1 else 0 isInt(1) //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define isInt⁰ as fun isInt¹(v: ?): ? { +//│ define isInt⁰ as fun isInt¹(v: Unknown): Int⁰ { //│ match v //│ Int⁰ => //│ return 1 @@ -287,7 +287,7 @@ isInt(1) //│ return 0 //│ end //│ }; -//│ define isBool⁰ as fun isBool¹(v: ?): ? { +//│ define isBool⁰ as fun isBool¹(v: Unknown): Int⁰ { //│ match v //│ Bool⁰ => //│ return 1 @@ -317,7 +317,7 @@ isBool(1) // `Object` needs no `typeinfo`: as the root of the reference hierarchy it is decided by representation alone. // Both halves of the split count - a class instance is a `$Object` struct, a tuple is a Wasm array. class Foo(val x) -fun isObj(v) = if v is Object then 1 else 0 +fun isObj(v: Any) = if v is Object then 1 else 0 isObj(Foo(1)) //│ Wasm result: //│ = 1 @@ -335,7 +335,7 @@ isObj("hi") //│ = 0 // Tuples are the only values lowered to Wasm arrays, so `Array` is exactly the array test. -fun isArr(v) = if v is Array then 1 else 0 +fun isArr(v: Any) = if v is Array then 1 else 0 isArr([1, 2]) //│ Wasm result: //│ = 1 diff --git a/hkmc2/shared/src/test/mlscript/wasm/NumericIntrinsicReview.mls b/hkmc2/shared/src/test/mlscript/wasm/NumericIntrinsicReview.mls index de63470ff9..0bbcc99230 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/NumericIntrinsicReview.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/NumericIntrinsicReview.mls @@ -21,7 +21,9 @@ if true then wasm.f64.const(1.5) else wasm.f64.const(2.5) :ge fun chooseFloat(b: Bool): Float64 = if b then 1.5 else wasm.f64.const(2.5) -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Num' at an unrelated type 'Float64' +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Num' at an unrelated type 'Float64' +//│ ║ l.23: fun chooseFloat(b: Bool): Float64 = if b then 1.5 else wasm.f64.const(2.5) +//│ ╙── ^^^ //│ ╔══[COMPILATION ERROR] WatBackend::result for Lit expression not implemented yet //│ ║ l.23: fun chooseFloat(b: Bool): Float64 = if b then 1.5 else wasm.f64.const(2.5) //│ ╙── ^^^ diff --git a/hkmc2/shared/src/test/mlscript/wasm/NumericTypes.mls b/hkmc2/shared/src/test/mlscript/wasm/NumericTypes.mls index fe69899422..ca271b71d4 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/NumericTypes.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/NumericTypes.mls @@ -289,7 +289,7 @@ wasm.f64.add(Boxed(wasm.i64.const(1), wasm.f64.const(1.5)).f, wasm.f64.const(0.2 //│ (func $entry (export "entry") (type $entry) (result f64) //│ (local $tmp i64) //│ (local $tmp1 f64) -//│ (local $tmp2 (ref null any)) +//│ (local $tmp2 (ref null $Boxed)) //│ (local $tmp3 f64) //│ (block (result f64) //│ (local.set $tmp diff --git a/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls b/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls index 2101e73d44..016e5cc837 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/ReplImports.mls @@ -27,15 +27,20 @@ counter //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) //│ (type $plus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any)))) -//│ (type $entry (func (result (ref null any)))) +//│ (type $entry (func (result (ref null i31)))) //│ (import "system" "plus_impl" (func $plus_impl (type $plus_impl))) -//│ (import "repl" "counter" (global $counter (mut (ref null any)))) -//│ (import "repl" "step" (global $step (mut (ref null any)))) -//│ (func $entry (export "entry") (type $entry) (result (ref null any)) -//│ (return -//│ (call $plus_impl -//│ (global.get $counter) -//│ (global.get $step)))) +//│ (import "repl" "counter" (global $counter (mut (ref null i31)))) +//│ (import "repl" "step" (global $step (mut (ref null i31)))) +//│ (func $entry (export "entry") (type $entry) (result (ref null i31)) +//│ (local $tmp (ref null any)) +//│ (block (result (ref null i31)) +//│ (local.set $tmp +//│ (call $plus_impl +//│ (global.get $counter) +//│ (global.get $step))) +//│ (return +//│ (ref.cast (ref null i31) +//│ (local.get $tmp))))) //│ (elem $entry declare func $entry)) //│ Wasm result: //│ = 3 @@ -52,13 +57,12 @@ unit //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) //│ (type $Unit (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) //│ (type $Unit_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $entry (func (result (ref $Unit)))) +//│ (type $entry (func (result (ref null $Unit)))) //│ (import "repl" "Unit_typeinfo" (global $Unit_typeinfo (ref $Unit_typeinfo))) //│ (import "repl" "Unit$inst" (global $Unit (mut (ref null $Unit)))) -//│ (func $entry (export "entry") (type $entry) (result (ref $Unit)) +//│ (func $entry (export "entry") (type $entry) (result (ref null $Unit)) //│ (return -//│ (ref.cast (ref $Unit) -//│ (global.get $Unit)))) +//│ (global.get $Unit))) //│ (elem $entry declare func $entry)) //│ Wasm result: //│ = {} @@ -157,7 +161,18 @@ narrowedBox // The imported type must also enforce the cast, rather than just avoiding a missing-type diagnostic. :noInline :re -fun rejectNonBox(x): CastBox = x +fun rejectNonBox(x: Any): CastBox = x rejectNonBox(42) //│ Wasm result: //│ ═══[RUNTIME ERROR] RuntimeError: illegal cast + + +// An inferred session global of primitive type is stored unboxed as well. +let inferred = wasm.i32.const(123) +//│ Wasm result: +//│ = {} + +:expect 123 +inferred +//│ Wasm result: +//│ = 123 diff --git a/hkmc2/shared/src/test/mlscript/wasm/ScopedLocals.mls b/hkmc2/shared/src/test/mlscript/wasm/ScopedLocals.mls index 339bca5eee..f6ec8df761 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/ScopedLocals.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/ScopedLocals.mls @@ -13,8 +13,8 @@ topA + topB //│ (type $plus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any)))) //│ (type $entry (func (result (ref null any)))) //│ (import "system" "plus_impl" (func $plus_impl (type $plus_impl))) -//│ (global $topA (export "topA") (mut (ref null any)) (ref.null any)) -//│ (global $topB (export "topB") (mut (ref null any)) (ref.null any)) +//│ (global $topA (export "topA") (mut (ref null i31)) (ref.null i31)) +//│ (global $topB (export "topB") (mut (ref null i31)) (ref.null i31)) //│ (func $entry (export "entry") (type $entry) (result (ref null any)) //│ (block (result (ref null any)) //│ (global.set $topA @@ -44,11 +44,11 @@ chain(1) //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $chain (func (param $x (ref null any)) (result (ref null any)))) +//│ (type $chain (func (param $x (ref null i31)) (result (ref null any)))) //│ (type $plus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any)))) //│ (type $entry (func (result (ref null any)))) //│ (import "system" "plus_impl" (func $plus_impl (type $plus_impl))) -//│ (func $chain (export "chain") (type $chain) (param $x (ref null any)) (result (ref null any)) +//│ (func $chain (export "chain") (type $chain) (param $x (ref null i31)) (result (ref null any)) //│ (local $a (ref null any)) //│ (local $b (ref null any)) //│ (block (result (ref null any)) @@ -145,15 +145,15 @@ new Foo(42, 1).Foo#a //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $Foo (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $a (mut (ref null any))) (field $b (mut (ref null any)))))) +//│ (type $Foo (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $a (mut (ref null i31))) (field $b (mut (ref null i31)))))) //│ (type $Foo_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) -//│ (type $Foo_init (func (param $this (ref $Foo)) (param $a (ref null any)) (param $b (ref null any)) (result (ref $Foo)))) -//│ (type $Foo_ctor (func (param $a (ref null any)) (param $b (ref null any)) (result (ref $Foo)))) -//│ (type $entry (func (result (ref null any)))) +//│ (type $Foo_init (func (param $this (ref $Foo)) (param $a (ref null i31)) (param $b (ref null i31)) (result (ref $Foo)))) +//│ (type $Foo_ctor (func (param $a (ref null i31)) (param $b (ref null i31)) (result (ref $Foo)))) +//│ (type $entry (func (result (ref null i31)))) //│ (global $Foo_typeinfo (export "Foo_typeinfo") (ref $Foo_typeinfo) (struct.new $Foo_typeinfo //│ (i32.const 1) //│ (ref.null $TypeInfoBase))) -//│ (func $Foo_init (type $Foo_init) (param $this (ref $Foo)) (param $a (ref null any)) (param $b (ref null any)) (result (ref $Foo)) +//│ (func $Foo_init (type $Foo_init) (param $this (ref $Foo)) (param $a (ref null i31)) (param $b (ref null i31)) (result (ref $Foo)) //│ (block (result (ref $Foo)) //│ (struct.set $Foo $a //│ (local.get $this) @@ -164,14 +164,14 @@ new Foo(42, 1).Foo#a //│ (return //│ (ref.cast (ref $Foo) //│ (local.get $this))))) -//│ (func $Foo_ctor (export "Foo") (type $Foo_ctor) (param $a (ref null any)) (param $b (ref null any)) (result (ref $Foo)) +//│ (func $Foo_ctor (export "Foo") (type $Foo_ctor) (param $a (ref null i31)) (param $b (ref null i31)) (result (ref $Foo)) //│ (local $this (ref $Foo)) //│ (block (result (ref $Foo)) //│ (local.set $this //│ (struct.new $Foo //│ (global.get $Foo_typeinfo) -//│ (ref.null any) -//│ (ref.null any))) +//│ (ref.null i31) +//│ (ref.null i31))) //│ (drop //│ (call $Foo_init //│ (local.get $this) @@ -179,9 +179,9 @@ new Foo(42, 1).Foo#a //│ (local.get $b))) //│ (return //│ (local.get $this)))) -//│ (func $entry (export "entry") (type $entry) (result (ref null any)) -//│ (local $tmp (ref $Foo)) -//│ (block (result (ref null any)) +//│ (func $entry (export "entry") (type $entry) (result (ref null i31)) +//│ (local $tmp (ref null $Foo)) +//│ (block (result (ref null i31)) //│ (local.set $tmp //│ (call $Foo_ctor //│ (ref.i31 @@ -190,7 +190,8 @@ new Foo(42, 1).Foo#a //│ (i32.const 1)))) //│ (return //│ (struct.get $Foo $a -//│ (local.get $tmp))))) +//│ (ref.cast (ref $Foo) +//│ (local.get $tmp)))))) //│ (elem $Foo_init declare func $Foo_init) //│ (elem $Foo_ctor declare func $Foo_ctor) //│ (elem $entry declare func $entry)) diff --git a/hkmc2/shared/src/test/mlscript/wasm/SingletonUnit.mls b/hkmc2/shared/src/test/mlscript/wasm/SingletonUnit.mls index c0b6cfcc90..6c494c9a80 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/SingletonUnit.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/SingletonUnit.mls @@ -11,7 +11,7 @@ //│ (type $Unit (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) //│ (type $Unit_init (func (param $this (ref $Unit)) (result (ref $Unit)))) //│ (type $Unit_ctor (func (result (ref $Unit)))) -//│ (type $entry (func (result (ref $Unit)))) +//│ (type $entry (func (result (ref null $Unit)))) //│ (type $start (func)) //│ (global $Unit_typeinfo (export "Unit_typeinfo") (ref $Unit_typeinfo) (struct.new $Unit_typeinfo //│ (i32.const 1) @@ -35,10 +35,9 @@ //│ (func $start (type $start) //│ (global.set $Unit$inst //│ (call $Unit_Unit))) -//│ (func $entry (export "entry") (type $entry) (result (ref $Unit)) +//│ (func $entry (export "entry") (type $entry) (result (ref null $Unit)) //│ (return -//│ (ref.cast (ref $Unit) -//│ (global.get $Unit$inst)))) +//│ (global.get $Unit$inst))) //│ (elem $Unit_init declare func $Unit_init) //│ (elem $Unit_Unit declare func $Unit_Unit) //│ (elem $start declare func $start) @@ -86,7 +85,7 @@ x //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) //│ (type $entry (func (result (ref null i31)))) -//│ (global $x (export "x") (mut (ref null any)) (ref.null any)) +//│ (global $x (export "x") (mut (ref null i31)) (ref.null i31)) //│ (func $entry (export "entry") (type $entry) (result (ref null i31)) //│ (block (result (ref null i31)) //│ (global.set $x diff --git a/hkmc2/shared/src/test/mlscript/wasm/Singletons.mls b/hkmc2/shared/src/test/mlscript/wasm/Singletons.mls index 3664c2ae24..ea43e64d1a 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/Singletons.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/Singletons.mls @@ -12,15 +12,15 @@ Bar.y //│ (module //│ (type $TypeInfoBase (sub (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Object (sub (struct (field $$typeinfo (mut (ref $TypeInfoBase)))))) -//│ (type $Foo (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any)))))) -//│ (type $Bar (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $y (mut (ref null any)))))) +//│ (type $Foo (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null i31)))))) +//│ (type $Bar (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $y (mut (ref null i31)))))) //│ (type $Foo_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Foo_init (func (param $this (ref $Foo)) (result (ref $Foo)))) //│ (type $Foo_ctor (func (result (ref $Foo)))) //│ (type $Bar_typeinfo (sub $TypeInfoBase (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase))))) //│ (type $Bar_init (func (param $this (ref $Bar)) (result (ref $Bar)))) //│ (type $Bar_ctor (func (result (ref $Bar)))) -//│ (type $entry (func (result (ref null any)))) +//│ (type $entry (func (result (ref null i31)))) //│ (type $start (func)) //│ (global $Foo_typeinfo (export "Foo_typeinfo") (ref $Foo_typeinfo) (struct.new $Foo_typeinfo //│ (i32.const 1) @@ -45,7 +45,7 @@ Bar.y //│ (local.set $this //│ (struct.new $Foo //│ (global.get $Foo_typeinfo) -//│ (ref.null any))) +//│ (ref.null i31))) //│ (drop //│ (call $Foo_init //│ (local.get $this))) @@ -66,7 +66,7 @@ Bar.y //│ (local.set $this //│ (struct.new $Bar //│ (global.get $Bar_typeinfo) -//│ (ref.null any))) +//│ (ref.null i31))) //│ (drop //│ (call $Bar_init //│ (local.get $this))) @@ -78,7 +78,7 @@ Bar.y //│ (call $Foo_ctor)) //│ (global.set $Bar$inst //│ (call $Bar_ctor)))) -//│ (func $entry (export "entry") (type $entry) (result (ref null any)) +//│ (func $entry (export "entry") (type $entry) (result (ref null i31)) //│ (return //│ (struct.get $Bar $y //│ (ref.cast (ref $Bar) diff --git a/hkmc2/shared/src/test/mlscript/wasm/TailRecOptCasts.mls b/hkmc2/shared/src/test/mlscript/wasm/TailRecOptCasts.mls index 9d7514933f..c476ef89da 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/TailRecOptCasts.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/TailRecOptCasts.mls @@ -269,7 +269,9 @@ broadExit(1) :ge fun sccF(): Int31 = wasm.ref.i31(sccG()) fun sccG(): Int32 = wasm.i31.get_s(sccF()) + wasm.i32.const(1) -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Unknown' at an unrelated type 'Int32' +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Unknown' at an unrelated type 'Int32' +//│ ║ l.271: fun sccG(): Int32 = wasm.i31.get_s(sccF()) + wasm.i32.const(1) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Types 'Int31' and 'Int32' have no common representation //│ ║ l.271: fun sccG(): Int32 = wasm.i31.get_s(sccF()) + wasm.i32.const(1) //│ ╙── ^^^^ @@ -287,12 +289,7 @@ fun sccG(): Int32 = wasm.i31.get_s(sccF()) + wasm.i32.const(1) //│ ═══[COMPILATION ERROR] Types 'Int31' and 'Int32' have no common representation //│ ═══[COMPILATION ERROR] Types 'Int31' and 'Int32' have no common representation //│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `(ref null any)` to `i32` in a different type hierarchy -//│ ╔══[COMPILATION ERROR] Wasm function `sccF` expects result to have type `(ref null i31)`, but got `(ref null any)` -//│ ║ l.270: fun sccF(): Int31 = wasm.ref.i31(sccG()) -//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -//│ ╔══[COMPILATION ERROR] Wasm function `sccG` expects result to have type `i32`, but got `(ref null any)` -//│ ║ l.271: fun sccG(): Int32 = wasm.i31.get_s(sccF()) + wasm.i32.const(1) -//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `(ref null any)` to `i32` in a different type hierarchy // `bar` on its own first, so the `:fixme` below is known to be about the mutual recursion rather than about @@ -309,6 +306,6 @@ fun bar(m: Int32) = foo(wasm.ref.i31(m)) //│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `i32` to `(ref null any)` in a different type hierarchy //│ ╔══[COMPILATION ERROR] Operand #1 of wasm intrinsic 'ref.i31' must be of type 'Int32' -//│ ║ l.309: foo(wasm.ref.i31(m)) +//│ ║ l.306: foo(wasm.ref.i31(m)) //│ ╙── ^ //│ ═══[COMPILATION ERROR] Cannot cast a Wasm value of type `i32` to `(ref null any)` in a different type hierarchy diff --git a/hkmc2/shared/src/test/mlscript/wasm/VirtualMethods.mls b/hkmc2/shared/src/test/mlscript/wasm/VirtualMethods.mls index 057b84255a..ecc3bc5188 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/VirtualMethods.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/VirtualMethods.mls @@ -32,7 +32,7 @@ callF(B()) //│ (type $B_typeinfo (sub $A_typeinfo (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase)) (field $slot0 (mut (ref null $virtual2)))))) //│ (type $B_init (func (param $this (ref $B)) (result (ref $B)))) //│ (type $B_ctor (func (result (ref $B)))) -//│ (type $callF (func (param $a (ref $A)) (result (ref null any)))) +//│ (type $callF (func (param $a (ref null $A)) (result (ref null any)))) //│ (type $plus_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any)))) //│ (type $times_impl (func (param $lhs (ref null any)) (param $rhs (ref null any)) (result (ref null any)))) //│ (type $entry (func (result (ref null any)))) @@ -92,7 +92,7 @@ callF(B()) //│ (local.get $x) //│ (ref.i31 //│ (i32.const 2))))) -//│ (func $callF (export "callF") (type $callF) (param $a (ref $A)) (result (ref null any)) +//│ (func $callF (export "callF") (type $callF) (param $a (ref null $A)) (result (ref null any)) //│ (local $receiver (ref null any)) //│ (return //│ (block (result (ref null any)) @@ -109,19 +109,15 @@ callF(B()) //│ (ref.cast (ref $Object) //│ (local.get $receiver))))))))) //│ (func $entry (export "entry") (type $entry) (result (ref null any)) -//│ (local $tmp (ref null any)) -//│ (local $a (ref $A)) +//│ (local $tmp (ref null $B)) //│ (local $receiver (ref null any)) //│ (block (result (ref null any)) //│ (local.set $tmp //│ (call $B_ctor)) -//│ (local.set $a -//│ (ref.cast (ref $A) -//│ (local.get $tmp))) //│ (return //│ (block (result (ref null any)) //│ (local.set $receiver -//│ (local.get $a)) +//│ (local.get $tmp)) //│ (call_ref $virtual2 //│ (ref.cast (ref $A) //│ (local.get $receiver)) @@ -191,7 +187,7 @@ callPrim(W()) //│ (type $W_typeinfo (sub $V_typeinfo (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase)) (field $slot0 (mut (ref null $virtual2)))))) //│ (type $W_init (func (param $this (ref $W)) (result (ref $W)))) //│ (type $W_ctor (func (result (ref $W)))) -//│ (type $callPrim (func (param $a (ref $V)) (result i32))) +//│ (type $callPrim (func (param $a (ref null $V)) (result i32))) //│ (type $entry (func (result i32))) //│ (global $n32 (export "n32") (mut i32) (i32.const 0)) //│ (global $V_typeinfo (export "V_typeinfo") (ref $V_typeinfo) (struct.new $V_typeinfo @@ -242,7 +238,7 @@ callPrim(W()) //│ (func $W_f (type $virtual2) (param $this (ref $V)) (param $m i32) (result i32) //│ (return //│ (local.get $m))) -//│ (func $callPrim (export "callPrim") (type $callPrim) (param $a (ref $V)) (result i32) +//│ (func $callPrim (export "callPrim") (type $callPrim) (param $a (ref null $V)) (result i32) //│ (local $receiver (ref null any)) //│ (return //│ (block (result i32) @@ -259,8 +255,7 @@ callPrim(W()) //│ (local.get $receiver))))))))) //│ (func $entry (export "entry") (type $entry) (result i32) //│ (local $tmp i32) -//│ (local $tmp1 (ref null any)) -//│ (local $a (ref $V)) +//│ (local $tmp1 (ref null $W)) //│ (local $receiver (ref null any)) //│ (block (result i32) //│ (local.set $tmp @@ -269,13 +264,10 @@ callPrim(W()) //│ (local.get $tmp)) //│ (local.set $tmp1 //│ (call $W_ctor)) -//│ (local.set $a -//│ (ref.cast (ref $V) -//│ (local.get $tmp1))) //│ (return //│ (block (result i32) //│ (local.set $receiver -//│ (local.get $a)) +//│ (local.get $tmp1)) //│ (call_ref $virtual2 //│ (ref.cast (ref $V) //│ (local.get $receiver)) @@ -304,25 +296,51 @@ class M1() with class M2() extends M1() with override fun f(m: Int32): Int = 0 M2().f(1) +//│ ╔══[COMPILATION ERROR] Overriding member 'f' produces values represented as 'Int', which the member it overrides cannot produce: its results are represented as 'Int32' +//│ ║ l.297: override fun f(m: Int32): Int = 0 +//│ ║ ^ +//│ ╟── The overridden member 'f' is defined here +//│ ║ l.295: open fun f(m: Int32): Int32 = m +//│ ╙── ^ //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Int32' -//│ ║ l.306: M2().f(1) +//│ ║ l.298: M2().f(1) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Overriding method `f` declares result as `(ref null i31)`, but the method it overrides declares `i32` -//│ ║ l.305: override fun f(m: Int32): Int = 0 +//│ ║ l.297: override fun f(m: Int32): Int = 0 //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^ -// The same rule on a parameter: `g`'s `m` is unannotated, so it erases to `anyref` rather than `i32`. +// The same rule on a parameter. An unannotated parameter of an override takes the inherited +// representation, so only a declared one can differ: here `m: Int` erases to `anyref` rather than `i32`. :ge class N1() with open fun g(m: Int32): Int32 = m class N2() extends N1() with - override fun g(m): Int32 = 0 -N2().g(1) -//│ ═══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Int32' -//│ ╔══[COMPILATION ERROR] Overriding method `g` declares parameter 1 as `(ref null any)`, but the method it overrides declares `i32` -//│ ║ l.320: override fun g(m): Int32 = 0 -//│ ╙── ^^^^^^^^^^^^^^^^^^^ + override fun g(m: Int): Int32 = n32 +N2().g(n32) +//│ ╔══[COMPILATION ERROR] Parameter 'm' of overriding member 'g' is represented as 'Int', which cannot hold the values represented as 'Int32' that the member it overrides accepts +//│ ║ l.319: override fun g(m: Int): Int32 = n32 +//│ ║ ^^^^^^ +//│ ╟── The overridden member 'g' is defined here +//│ ║ l.317: open fun g(m: Int32): Int32 = m +//│ ╙── ^ +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int32' at an unrelated type 'Int' +//│ ║ l.320: N2().g(n32) +//│ ╙── ^^^ +//│ ╔══[COMPILATION ERROR] Overriding method `g` declares parameter 1 as `(ref null i31)`, but the method it overrides declares `i32` +//│ ║ l.319: override fun g(m: Int): Int32 = n32 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +// An unannotated parameter of an override takes the inherited `i32` representation. +:expect 7 +class O1() with + open fun g(m: Int32): Int32 = m +class O2() extends O1() with + override fun g(m) = m +O2().g(n32) +//│ Wasm result: +//│ = 7 // Boxed types are bridged by casts, so they need not match @@ -359,8 +377,8 @@ readChild(new Child()) //│ (type $Child_typeinfo (sub $Parent_typeinfo (struct (field $$tag i32) (field $$parent (ref null $TypeInfoBase)) (field $slot0 (mut (ref null $virtual1)))))) //│ (type $Child_init (func (param $this (ref $Child)) (result (ref $Child)))) //│ (type $Child_ctor (func (result (ref $Child)))) -//│ (type $readChild (func (param $c (ref $Child)) (result (ref $Child)))) -//│ (type $entry (func (result (ref $Child)))) +//│ (type $readChild (func (param $c (ref null $Child)) (result (ref null $Child)))) +//│ (type $entry (func (result (ref null $Child)))) //│ (global $Parent_typeinfo (export "Parent_typeinfo") (ref $Parent_typeinfo) (struct.new $Parent_typeinfo //│ (i32.const 1) //│ (ref.null $TypeInfoBase) @@ -409,13 +427,13 @@ readChild(new Child()) //│ (func $Child_get (type $virtual1) (param $this (ref $Parent)) (result (ref null any)) //│ (return //│ (call $Child_ctor))) -//│ (func $readChild (export "readChild") (type $readChild) (param $c (ref $Child)) (result (ref $Child)) +//│ (func $readChild (export "readChild") (type $readChild) (param $c (ref null $Child)) (result (ref null $Child)) //│ (local $receiver (ref null any)) //│ (return -//│ (block (result (ref $Child)) +//│ (block (result (ref null $Child)) //│ (local.set $receiver //│ (local.get $c)) -//│ (ref.cast (ref $Child) +//│ (ref.cast (ref null $Child) //│ (call_ref $virtual1 //│ (ref.cast (ref $Parent) //│ (local.get $receiver)) @@ -424,9 +442,9 @@ readChild(new Child()) //│ (struct.get $Object $$typeinfo //│ (ref.cast (ref $Object) //│ (local.get $receiver)))))))))) -//│ (func $entry (export "entry") (type $entry) (result (ref $Child)) -//│ (local $tmp (ref $Child)) -//│ (block (result (ref $Child)) +//│ (func $entry (export "entry") (type $entry) (result (ref null $Child)) +//│ (local $tmp (ref null $Child)) +//│ (block (result (ref null $Child)) //│ (local.set $tmp //│ (call $Child_ctor)) //│ (return @@ -442,3 +460,42 @@ readChild(new Child()) //│ (elem $entry declare func $entry)) //│ Wasm result: //│ = {} + + +// An override must produce only representations of the inherited result. `Producer.get`'s result is inferred +// from its body, so an override returning a string is rejected, rather than trapping when its result is cast +// to the result type of the shared slot. +:ge +class Producer() with + open fun get() = new Producer() +class StrProducer() extends Producer() with + override fun get() = "str" +fun produce(p) = p.get() +produce(StrProducer()) +//│ ╔══[COMPILATION ERROR] Overriding member 'get' produces values represented as 'Str', which the member it overrides cannot produce: its results are represented as 'Producer' +//│ ║ l.472: override fun get() = "str" +//│ ║ ^^^ +//│ ╟── The overridden member 'get' is defined here +//│ ║ l.470: open fun get() = new Producer() +//│ ╙── ^^^ +//│ Wasm result: +//│ ═══[RUNTIME ERROR] RuntimeError: illegal cast + + +// An unannotated parameter of an override takes the inherited representation, inferred here from calls through +// `Consumer`. A direct call passing a string is rejected, rather than trapping on the cast to the shared slot's +// parameter type. +:ge +class Consumer() with + open fun take(x) = x +class EchoConsumer() extends Consumer() with + override fun take(x) = x +fun consume(c) = c.take(1) +consume(Consumer()) +consume(EchoConsumer()) +EchoConsumer().take("s") +//│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Str' at an unrelated type 'Int' +//│ ║ l.496: EchoConsumer().take("s") +//│ ╙── ^^^ +//│ Wasm result: +//│ ═══[RUNTIME ERROR] RuntimeError: illegal cast diff --git a/hkmc2/shared/src/test/mlscript/wasm/WasmScratch.mls b/hkmc2/shared/src/test/mlscript/wasm/WasmScratch.mls new file mode 100644 index 0000000000..a5b13f151c --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/wasm/WasmScratch.mls @@ -0,0 +1,3 @@ +:. + +