diff --git a/AGENTS.md b/AGENTS.md index 07d0cf536a..fb873a7357 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,11 @@ Note that this only affects the main compilation tests; there is also `cntest` for nofib compilation tests, `catest` for application compilation tests, and `cwtest` for WASM compilation tests. -Before finishing your work, run `hkmc2AllTests/test` to make sure the codebase compiles and all tests succeed. +Before finishing work with retained code or test changes, run `hkmc2AllTests/test` +to make sure the codebase compiles and all tests succeed; +this covers `ctest`, so no need to run `ctest` separately. +Full validation is not required for read-only investigations, documentation-only edits, +and temporary experiments whose changes have all been reverted. After you are done fixing all the problems and all the tests pass, *you need to commit the resulting golden test output changes*. @@ -30,11 +34,15 @@ Any commit that does not include the latest changes to test outputs will fail th Please also read the files in `.github/skills/hkmc2-difftests`. -Use `.mls` diff tests for compiler behavior. Do not add Scala tests that duplicate +Use `*.mls` diff tests for compiler behavior. Do not add Scala tests that duplicate that coverage or merely pin internal representation or assertion details. Reserve unit tests for properties diff tests cannot reliably observe, such as concurrency, cache isolation, bounded graph growth, or algebraic laws; state the extra coverage. +Before adding new diff tests, make sure they are not already covered by existing tests. +All new diff-test files you create should go into `hkmc2/shared/src/test/mlscript/slop/` +unless there is a good reason to put them elsewhere. + When you create a commit, author it under your (the agent's) own identity, not under the user's identity. Set this identity for the commit command only @@ -44,6 +52,12 @@ Do not change repository-local or global Git identity configuration. ## Design Philosophy +**Confirm language-design decisions before implementing them.** Propose changes +to source syntax, language semantics, or public library contracts and obtain the +user's confirmation unless that specific design is already explicitly approved. +Authorization to fix compiler behavior does not by itself authorize introducing +annotations, changing member naming or dispatch rules, or extending public APIs. + You are working on an industry-strength compiler codebase. Although its test suite is extensive, tests alone are not enough to determine the quality of a change; the goal is not just to "make everything green". diff --git a/README.md b/README.md index 8112bbd5d3..52392c65a3 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,7 @@ and then use one of the following commands. - `hkmc2AppsTests/test` for running the applications compile and diff tests. - `hkmc2NofibTests/test` for running the nofib compile and diff tests. - `hkmc2WasmTests/test` for running the wasm compile and diff tests. +- `hkmc2SlopTests/test` for running the AI-authored regression diff-tests in `hkmc2/shared/src/test/mlscript/slop`, which are excluded from the main diff-tests. - `hkmc2MostTests/test` for running all of the above. - `hkmc2AllTests/test` for running all default hkmc2 tests, including ScalaJS-compiled tests. - `~hkmc2DiffTests/Test/run` for running the test watcher, @@ -194,6 +195,11 @@ and then use one of the following commands. Individual tests can be run with `-z`. For example, `~mlscriptJVM/testOnly mlscript.DiffTests -- -z parser` will watch for file changes and continuously run all parser tests (those that have "parser" in their name). +Run `ctest` before running individual diff-tests, including `sdtest`, +so the generated JavaScript dependencies are available. +Also run `cwtest` before individual Wasm diff-tests, including those under `slop/wasm`. +`hkmc2SlopTests/test` runs the main and Wasm compilation prerequisites automatically. + #### Useful SBT commands aliases (use these for more pleasant development) The most useful command alias for development is `watch` for continuously running the test watcher, which: @@ -207,6 +213,7 @@ or specify a string to filter the tests to run. - `ctest` aka "main compilation tests" for running specific tests in `hkmc2/shared/src/test/mlscript-compile` - `dtest` aka "main diff-tests" for running only the main diff-tests in `hkmc2/shared/src/test/mlscript` +- `sdtest` for running only the diff-tests in `hkmc2/shared/src/test/mlscript/slop` - You can find other similar command aliases in `build.sbt`. The command aliases below are shorthand for running aggregated tests; they take no arguments. diff --git a/build.sbt b/build.sbt index 54567254a4..130cd347b8 100644 --- a/build.sbt +++ b/build.sbt @@ -93,18 +93,18 @@ lazy val hkmc2DiffTests = project.in(file("hkmc2DiffTests")) Test/run/fork := true, // so that CTRL+C actually terminates the watcher ) +val compileTestFixtures = taskKey[Unit]("Compile the MLscript fixtures needed by diff tests") + /** Helper to create test subprojects that compile `.mls` files then run diff tests. * Each subproject depends on `hkmc2JVM` and `hkmc2DiffTests` for shared test infrastructure. - * When a compile runner is provided, `Def.sequential` guarantees it completes before diff tests start. */ + * A named prerequisite lets suites share fixture compilation in aggregate runs. + * Diff tests wait for it before loading the generated modules. */ def hkmc2TestSubproject(dirName: String, compileRunner: Option[String], diffRunner: String): Project = { - val testTask = compileRunner match { + val compileTask = compileRunner match { case Some(runner) => - Def.sequential( - (Test / testOnly).toTask(s" hkmc2.$runner"), - (Test / testOnly).toTask(s" hkmc2.$diffRunner"), - ) + (Test / testOnly).toTask(s" hkmc2.$runner") case None => - (Test / testOnly).toTask(s" hkmc2.$diffRunner") + Def.task { () } } Project(dirName, file(dirName)) @@ -116,7 +116,9 @@ def hkmc2TestSubproject(dirName: String, compileRunner: Option[String], diffRunn libraryDependencies += "org.scalactic" %%% "scalactic" % scalaTestVersion, libraryDependencies += "org.scalatest" %%% "scalatest" % scalaTestVersion % "test", - Test / test := testTask.value, + Test / compileTestFixtures := compileTask.value, + Test / test := (Test / testOnly).toTask(s" hkmc2.$diffRunner") + .dependsOn(Test / compileTestFixtures).value, Test/run/fork := true, // so that CTRL+C actually terminates the watcher ) @@ -125,12 +127,23 @@ def hkmc2TestSubproject(dirName: String, compileRunner: Option[String], diffRunn lazy val hkmc2NofibTests = hkmc2TestSubproject("hkmc2NofibTests", Some("NofibCompileTestRunner"), "NofibDiffTestRunner") lazy val hkmc2AppsTests = hkmc2TestSubproject("hkmc2AppsTests", Some("AppsCompileTestRunner"), "AppsDiffTestRunner") lazy val hkmc2WasmTests = hkmc2TestSubproject("hkmc2WasmTests", Some("WasmCompileTestRunner"), "WasmDiffTestRunner") +lazy val hkmc2SlopTests = hkmc2TestSubproject("hkmc2SlopTests", None, "SlopDiffTestRunner") + .settings( + // These worksheets load main-suite fixtures such as Iter.mjs and the Wasm.mjs + // runtime helper. Both must be generated before the slop tests start, even + // when an aggregate schedules the Wasm and slop suites as sibling tasks. + Test / test := (Test / test).dependsOn( + hkmc2JVM / Test / test, + hkmc2WasmTests / Test / compileTestFixtures, + ).value, + ) lazy val hkmc2MainTests = project.in(file("hkmc2MainTests")) .settings( Test / test := ( (hkmc2DiffTests / Test / test) .dependsOn(hkmc2JVM / Test / test) + .dependsOn(hkmc2SlopTests / Test / test) ).value ) @@ -141,6 +154,7 @@ lazy val hkmc2MostTests = project.in(file("hkmc2MostTests")) .dependsOn(hkmc2NofibTests / Test / test) .dependsOn(hkmc2AppsTests / Test / test) .dependsOn(hkmc2WasmTests / Test / test) + .dependsOn(hkmc2SlopTests / Test / test) .dependsOn(hkmc2JVM / Test / test) ).value ) @@ -152,6 +166,7 @@ lazy val hkmc2AllTests = project.in(file("hkmc2AllTests")) .dependsOn(hkmc2NofibTests / Test / test) .dependsOn(hkmc2AppsTests / Test / test) .dependsOn(hkmc2WasmTests / Test / test) + .dependsOn(hkmc2SlopTests / Test / test) .dependsOn(hkmc2JVM / Test / test) .dependsOn(hkmc2JS / Test / test) .dependsOn(hkmc2Benchmarks / Test / compile) @@ -166,6 +181,7 @@ addCommandAlias("dtest", "hkmc2DiffTests/testOnly hkmc2.DiffTestRunner -- -z") addCommandAlias("ndtest", "hkmc2NofibTests/testOnly hkmc2.NofibDiffTestRunner -- -z") addCommandAlias("adtest", "hkmc2AppsTests/testOnly hkmc2.AppsDiffTestRunner -- -z") addCommandAlias("wdtest", "hkmc2WasmTests/testOnly hkmc2.WasmDiffTestRunner -- -z") +addCommandAlias("sdtest", "hkmc2SlopTests/testOnly hkmc2.SlopDiffTestRunner -- -z") // Compilation tests addCommandAlias("ctest", "hkmc2JVM/testOnly hkmc2.CompileTestRunner -- -z") addCommandAlias("cntest", "hkmc2NofibTests/testOnly hkmc2.NofibCompileTestRunner -- -z") diff --git a/doc/new-resolution-type-value-flow.md b/doc/new-resolution-type-value-flow.md index 2917ac5e07..fbd45f90e7 100644 --- a/doc/new-resolution-type-value-flow.md +++ b/doc/new-resolution-type-value-flow.md @@ -36,6 +36,16 @@ alternative constraint sets or forming products of intersection candidates. They deliberately approximate type inference; diagnosing concrete incompatibilities such as `Str <: Int` belongs to type checking, not resolution. +This distinction also applies to separate function signatures and callback +layouts. Skipping an uncertain compatibility check must not skip its flows. +Every callable component contributes its domain to the implementation parameters, +and later curried lists remain connected even when an earlier layout differs. +Each implementation parameter has a shared annotation endpoint that accumulates +these components; replacing it with the last component would disconnect earlier +constraints. Matching positions stay precise. Uncertain middle positions receive +the union of their possible inputs, including typed rest elements. Rest slicing +may widen lengths, but retains the deferred types of every possible element. + ## Instance wrappers and interface observations `InstanceShape(DeclaredType)` describes an instance of a type. Parameter/result @@ -135,7 +145,7 @@ input and output parts: | `out U` | `Nothing` | `U` | | `in T out U` | `T` | `U` | -Declaration variance applies to an unqualified argument. A written wildcard +Nominal declaration variance applies to an unqualified argument. A written wildcard supplies its own parts and overrides declaration variance. Comparing actual `a` with expected `b` installs `a.output <: b.output` and `b.input <: a.input`. Missing wildcard parts are actual top/bottom types, not inference holes. @@ -189,12 +199,26 @@ type checker. The annotated-array case in `VarianceSoundness.mls` resolves normally and, with type checking disabled, records the unsafe runtime write. InvalML regressions check rejection of the corresponding concrete mismatch. -Aliases bind both argument interpretations before interpreting their bodies at the -enclosing polarity. This avoids applying the enclosing reversal twice: -`Identity[T]`, for `type Identity[A] = A`, must behave like `T` in either polarity. -Captured enclosing binders retain their own bindings. Nominal applications instead -retain the argument pair composed with the application's polarity, since member -signatures subsequently interpret that pair from the member's positive interface. +Alias formals start a new substitution scope. References to already-bound +enclosing parameters inside an argument still select their bounds according to +their position in the enclosing signature. A fresh, unbound parameter remains +itself. Apply declaration variance (or the written wildcard) to that interpreted +argument, then select its endpoint according to its occurrence inside the alias body. +For `type Source[out T] = {item: T}`, both `Source[A]` and +`Source[in Nothing out A]` expose `A` in `item`, including when the application +occurs in a function domain. The enclosing domain affects the interpretation +of `A`, not whether the occurrence of `T` in `{item: T}` selects its output. + +The implementation keeps the alias body's enclosing polarity so captured outer +binders retain their interpretation. It therefore reverses only the new formal +bindings' stored endpoint pairs when that polarity is negative. A globally +negative occurrence of `T` then selects the original output endpoint, while a +further domain reversal inside the alias selects the original input endpoint. +This also keeps `Identity[A]` equivalent to `A` in both polarities. +Nominal applications retain their composed argument pairs directly: member +signatures subsequently interpret them from the member's positive interface. +`TransparentAliasPolarity.mls` covers shorthand and explicit projections; +`VarianceAliases.mls` covers captured binders and nested identity aliases. `DeclaredType.positive` records this lexical polarity, independently of a later constraint's direction. `TypeShape.Wildcard` retains written parts; @@ -208,6 +232,33 @@ Selections are cached by argument reference and polarity; selecting an existing selection is idempotent. Forwarding cycles retain subscriptions but stop repeated observation. For a fixed argument-reference set, selection adds at most two nodes per reference. +### Iteration and synthesized applications + +Resolution is a best-effort check, not a type checker. Implicit spread observations +must propagate known flows without rejecting an opaque or inconvenient protocol. +They preserve every union and intersection component. Member lookup combines +an intersection's interfaces, while constraints inspect each component independently. An unknown alternative joins the known elements; it never +replaces them. Definitively completed iterator results supply no element, while +a missing or unknown `done` still permits observing `value`. + +Each non-tuple spread operand owns one `IterationFlow`: stable sites for selecting +`Symbol.iterator`, calling it, selecting and calling `next`, and reading `done` +and `value`, plus one deferred element host. These observations use ordinary +member scope transport and call instantiation. The implicit zero-argument calls +omit compatibility diagnostics but retain argument and result flow. Tuple +construction sees one unknown-length segment per operand, regardless of the +number of incoming iterator or element alternatives. This prevents products of +alternatives from multiplying through recursive spreads. Imported producers keep +their canonical sites while publisher storage remains local to each consumer. + +Arrays use this same protocol through the prelude's symbol-keyed method. Direct +Array element-bound observation remains useful for array patterns and rest +constraints, where the operation describes Array storage rather than iteration. +The `appliedTypes` table interns synthesized applications by constructor symbol +and contextual arguments, sharing the mechanism for `Array[T]` and `Awaited[T]`. +It reifies symbolic argument references before observation; caching their current +values instead would lose later bounds and caller-specific substitutions. + ## Shared bodies and contextual constraints The inferred body is a shared graph. Instantiated views carry flat original-binder @@ -581,3 +632,54 @@ Worksheet coverage under `newres` includes `MutableArrays`, `ContextualInference generic call isolation, and escaping payloads. Deferred cases retain explicit regression expectations; see the [future-work reference](new-resolution-future-work.md). + + +### Fresh property keys + +`@annotations.jsSymbol(path)` identifies a property by a stable path through modules and +immutable vals. The terminal val's `@annotations.fresh` marker is a contract, +not a type check or a proof about its initializer. Resolver key identity includes +the receiver path, and direct immutable receiver aliases are normalized. Modules +use their module symbol, independently of a companion function or class overload. +Key recognition does not evaluate a val initializer or force alias expansion. + +Annotations on declared module members can be interpreted from the module's +syntax before its value definitions complete. This matters for prelude bootstrap: +`@fresh` must not require a runtime value from the declared +`annotations` module. Builtin `Symbol` lookup is lazy so interpreting an unrelated +annotation does not require `Symbol` to be present in an earlier prelude block. +Completed overload sets are published as soon as their definitions are available, +allowing a subsequent annotation to select through an earlier immutable val. + +Lowering records computed names as explicit class operands and selected keys as +explicit path operands. Transformations therefore retain their imports and +captures and substitute their symbols. Computed names are evaluated in the +class's enclosing scope. Lifting cannot move such a class out of the invocation +that evaluates its keys; doing so could reuse a key or read it before initialization. + +At block completion, resolution recovers an access path from the use site's +lexical scope to the key's binding, following enclosing modules when necessary. +Context updates preserve scope identity; entering a nested block creates a new +scope. Term definitions retain their binding scope, as module definitions do. +File bindings can also be reached through the compilation unit's existing default +export or private export ABI. Foreign provenance alone does not make a local +binding importable. + +The legacy resolver uses the same `AccessPath.findAccessPath` helper for implicit +companion references. It retains the module overload at each step, so a companion +of a constructor function is reached through its class value, including across +files and after lifting. + +The recovered expression is part of the resolved reference and is cloned and +lowered like other operands. Lowering does not maintain a separate inventory of +available bindings. A member's resolved declaration cannot supply a factory-local +key to an escaped instance's caller. Such accesses fail at the use site; nested +closures can still capture the key. These checks do not change candidate flows. +Overrides with a common key retain all their targets and share only the recovered +key operand. + +Private members of classes and modules cannot supply keys across files: their +generated accessor symbols are local to one emitted JavaScript module. Resolution +reports the inaccessible private member at the selection, and code generation +asserts this boundary as a compiler invariant. This restriction does not apply to +private file-level vals, which use the compilation unit's private export ABI. diff --git a/doc/reference.md b/doc/reference.md index 2878e79531..66f403adf6 100644 --- a/doc/reference.md +++ b/doc/reference.md @@ -248,15 +248,78 @@ fun foo(x: Int): Int = x + 1 fun bar(x: Int, y: Str): Bool = x > 0 ``` +### Optional Parameters + +A parameter followed by `?` is optional: callers may omit its argument, which is then `null`. + +```mlscript +fun greet(name: Str, greeting?: Str) = + (if greeting is null then "Hello, " else greeting + ", ") + name +print \ greet("Ana") // prints: Hello, Ana +print \ greet("Ana", "Hi") // prints: Hi, Ana +``` + +The annotation of an optional parameter includes `null`: `greeting?: Str` has type `Str | null`. +The `?` must directly follow the parameter: `y ?` and `y ?: Str` are not optional parameters. + +Optional parameters follow required ones. +Function, lambda, and constructor parameters can be optional; +context parameters (`using`) and variadic parameters cannot. + +Direct calls must supply every required argument, and no more arguments than the function accepts. +A function that receives extra arguments might ignore them at runtime (unspecified behavior). + +### Variadic Parameters + +A parameter prefixed with `...` collects any number of arguments into an array. +Parameters may follow it; they receive the last arguments. + +```mlscript +fun f(...xs) = xs +f(1, 2, 3) // = [1, 2, 3] +fun g(x, ...xs, y) = [x, xs, y] +g(1, 2, 3, 4) // = [1, [2, 3], 4] +g(1, 2) // = [1, [], 2] +``` + +The required parameters take the first and last arguments, the optional parameters take the remaining +ones from left to right, and the variadic parameter collects the rest: + +```mlscript +fun h(x, y?, ...xs, z) = [x, y, xs, z] +h(1, 2) // = [1, undefined, [], 2] +h(1, 2, 3) // = [1, 2, [], 3] +h(1, 2, 3, 4, 5) // = [1, 2, [3, 4], 5] +``` + +A parameter list has at most one variadic parameter, and the parameters following it must be required. +Class parameters cannot be variadic. + +### Droppable Parameters + +`drop` marks a parameter that implementations may ignore and may even omit to specify. +In a function type, it lets the functions passed for it leave out that parameter: + +```mlscript +fun app(f: (Int, drop Int) -> Int) = f(1, 2) +app(x => x) // = 1 +app((x, i) => x + i) // = 3 +``` + +A variadic parameter can also be droppable, as in `(Int, drop ...Array[Int]) -> Int`. +Callers of a definition such as `fun first(x, drop y) = x` must still pass the argument of `y`. +Only function and lambda parameters can be droppable. + ### Recursive Functions -Functions are recursive by default: +Functions can make recursive calls: ```mlscript fun fact(n) = if n <= 1 then 1 else n * fact(n - 1) ``` -Mutual recursion uses sequential `fun` definitions in the same block. +Mutual recursion is permitted among `fun` definitions in the same block +or through method calls. ### Trailing arguments @@ -289,6 +352,7 @@ tuple x => x + 1 (x, y) => x + y () => 42 +=> 42 // also a zero-argument lambda ``` ### Underscore lambda shorthand @@ -342,6 +406,17 @@ xs.map(f) let f = => _ + 1 in xs.map(f) ``` +An explicit prefix `=>` always creates a lambda. If it contains no `_` placeholders, +the lambda takes zero arguments: `=> new C()` constructs a fresh instance whenever +called. Implicit delimiter positions such as `f \ expr` and `value |> expr` keep +expressions without placeholders eager. + +A bare `new C` partially applies a constructor when `C` has a parameter list. +If its only list accepts zero arguments (including an all-optional list), the +compiler warns: write `new C()` to construct the instance, or `=> new C()` for +an explicit zero-argument factory. A class declared without parameter lists +still constructs immediately with `new C`. + ### More examples ```fs @@ -781,6 +856,8 @@ fun g(x, ...rest) = rest f(1, 2, 3) // xs = [1, 2, 3] ``` +See [Variadic Parameters](#variadic-parameters). + Under new resolution, an annotation on a rest parameter describes the whole rest array. Tuple values support precise zero-based projections such as `xs.0` and inherit the declared Array interface. @@ -798,10 +875,10 @@ An explicit annotation such as `Array[Base]` makes element reads use the members declared by `Base`. Storing a `Child` that extends `Base` does not permit access to `Child`-only members through that annotation. -Array callbacks such as `map` receive the element, index, and array; their -declared function interfaces must account for these arguments, for example with -unused or rest parameters. `reduce` also passes the accumulator first, and must be -given an initial value: `xs.reduce((acc, x, ...) => acc + x, 0)`. Without one, +Array callbacks such as `map` receive the element, index, and array. The index and +array parameters are [droppable](#droppable-parameters), so callbacks may take only +the element: `xs.map(x => x + 1)`. `reduce` also passes the accumulator first, and must +be given an initial value: `xs.reduce((acc, x) => acc + x, 0)`. Without one, JavaScript uses the first element as the initial accumulator, which the declared signature does not describe. @@ -1365,28 +1442,86 @@ fun f(x: Int): Str = String(x) val v: Option[Int] = Some(1) ``` -### Resolution Interfaces -With new resolution (`#lang(0.3.x)`), annotations determine the interface available -to member lookup and calls. A receiver annotated `Base` exposes Base's declared -members and preserves virtual dispatch; concrete arguments cannot add subclass -members to that interface. The same rule applies to parameters, result annotations, -separate signatures, and constructor fields. Reading an unannotated member through -an annotated interface currently does not infer its shape from its initializer or -method body. Inference of missing selected-member types is a planned improvement. -Structural record annotations likewise expose only their declared fields. - -Compilation files check that their exposed functions and values work for inputs -admitted by their interfaces. A public `fun read(x) = x.a` needs an interface for +### Spreads + +Argument and array spreads follow the JavaScript iteration protocol: +`value[Symbol.iterator]().next().value`. Arrays, strings, native collections, +and user iterables use the same resolution path. Precise tuple spreads +retain their fixed positions. + + +### Resolution + +The resolution pass of the compiler finds targets for names, members, and calls, and catches some obvious mistakes. +It does so by taking into account the declared types of values and parameters +and by tracking the flow of values through the current compilation unit, +stopping at the unit's exposed public interface (to support separate and scalable compilation). +This pass is mandatory and affects the program's runtime behavior, +unlike type checking, which is optional. + +**Resolution is a best-effort process, not a replacement for type checking.** +Successful resolution does not establish type safety, even in strict mode. +Type checking is meant to run as a later pass, but is not yet implemented. + +Resolution is "loose" and "lenient" by default: while it rejects unresolved references, +it allows references that are ambiguous (see: loose) and uncertain (see: lenient). +Optionally, one can enable +* **strict** resolution, under which all references must be resolved to exactly one source target. + For example, `(xs: (List | Str)) => xs.length` is rejected: + while `List` and `Str` both have a `length` member, + and while that member could be selected dynamically under, eg, JS compilation, + the two members are unrelated, so the access could not be compiled directly + with more static backends like WASM. + On the other hand, `(xs: (Cons | Nil)) => xs.length` works, + provided that `Cons` and `Nil` are both subtypes of a common abstract class `List` that declares `length`. +* **eager** resolution: unlike lenient resolution, it does not discard receiver shapes that do not have the selected member. + This moves resolution a bit closer to type checking, but still does not guarantee type safety. + Resolution is lenient by default because it is quite approximate, + so it will often get spurious candidate receivers that are only present due to its approximate tracking of flow. + +Strict resolution is currently required by backends like WASM, +although smarter compilation strategies could allow more lenient resolution in the future +(an ambiguous selection can often be compiled into a match with unambiguous selections in its branches). + +`@annotations.jsSymbol(path)` gives a public method a source name for the JavaScript property +key denoted by `path`. The path must consist of modules and immutable `val`s, +and end in a `val` annotated with `@annotations.fresh`. This marker is a contract +that the val owns a unique symbol; the resolver does not prove freshness. + +```mlscript +module Keys { + @fresh val read = Symbol("read") +} +class Reader { + @jsSymbol(Keys.read) fun read() = 42 +} +``` + +The method is stored at `Keys.read`, and both `reader.read()` and +`reader.(Keys.read)()` select that property. An override must retain both the +inherited source name and the same key. Giving an inherited key a different +source name is a collision, as is defining two methods with the same key in one +class. Distinct receiver paths are significant for fresh instance fields. + +Currently, selecting a member on an escaped instance is unsupported when the +caller cannot access its local key: the compiler needs a runtime witness for +that particular key. Resolution diagnoses such accesses at the use site before +constructing a reference to the unavailable key. Closures that capture the key +remain valid, as do keys available through an imported file, including its +private file-level vals. The key must not be hoisted and shared between factory +invocations. `slop/JsSymbol.mls` records these cases. + +A key path that accesses a private member of a class or module in another file +is rejected at the selection site. Such members use private accessor symbols +local to their defining file; they do not use the file-level private export ABI. + +Compilation files check that their exposed functions and values do not make a closed-world assumption. A public `fun read(x) = x.a` needs an interface for `x` even if local callers all provide `{a: 1}`. A private helper can use local inference, but returning it or storing it in an exposed record or tuple makes its interface subject to the same check. Callable result annotations provide the declared parameter interface when checking returned functions. -Worksheet blocks perform this exposed-interface check only under -`#lang(strictResolution: true)`. Non-strict resolution allows multiple selection -or call targets, but still rejects missing targets and known invalid operations. - Generic parameters remain opaque regardless of strict mode, visibility, or local callers. For example, `private fun read[A](x: A) = x.a` is invalid because `A` does not supply member `a`, even when every call supplies a record with that field. @@ -1504,8 +1639,33 @@ arguments. Its diagnostic indicates a limitation of the current check. ```mlscript type Transformer[A, B] = A -> B type BinOp[A] = (A, A) -> A +type Callback = (Int, Int?) -> Int // optional parameter +type Labeled = (x: Int, y?: Int) -> Int // named parameters +type Step = (Int, drop Int) -> Int // droppable parameter +type Join = (Str, ...Array[Str], Str) -> Str // variadic parameter +``` + +Function type parameters follow the rules of +[optional](#optional-parameters), [variadic](#variadic-parameters), and [droppable](#droppable-parameters) +function parameters. Parameter names in function types are labels only: they do not affect compatibility. + +A function passed where a function type is expected must accept the arguments that the type passes: +- it may leave out trailing droppable parameters, including a droppable variadic parameter, + but no other parameters; +- it may not require an argument that the type marks as optional; +- its additional parameters must be optional, and are then `undefined`; +- it may collect several arguments in a variadic parameter. + +```mlscript +fun app(f: (Int, Int?) -> Int) = f(1) +app((x, y?) => x) // ok +app((x, y) => x) // error: `y` is required, but its argument may be omitted +app(x => x) // error: the optional parameter is not droppable ``` +A separate signature, as in `fun f: (Int, Int?) -> Int` followed by `fun f(x, y?) = x`, +must agree with the definition on which parameters are required, optional, and variadic. + ### `forall` (Universal Quantification) ```mlscript diff --git a/hkmc2/jvm/src/test/scala/hkmc2/PublisherTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/PublisherTest.scala index 8b9cf8e1c7..0d4292715b 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/PublisherTest.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/PublisherTest.scala @@ -3,7 +3,7 @@ package hkmc2 import org.scalatest.funsuite.AnyFunSuite import scala.collection.mutable.ArrayBuffer -import hkmc2.utils.* +import hkmc2.utils.*, shorthands.* import hkmc2.semantics.{Elaborator, ResolutionContext, Term, VarSymbol, DynShape, OpaqueTypeShape, TermShape} @@ -140,7 +140,7 @@ class PublisherTest extends AnyFunSuite: given owner: Elaborator.State = new Elaborator.State given state: ResolutionContext = owner.newResolverState.context val variable = new VarSymbol(new syntax.Tree.Ident("x")) - val use = Term.NewSel(Term.SimpleRef(variable)(new syntax.Tree.Ident("x")), + val use = Term.NewSel(Term.SimpleRef(variable)(S(new syntax.Tree.Ident("x"))), new syntax.Tree.Ident("value"), None)(semantics.FlowSymbol.sel("value")) use.hasDynamicTarget = true variable.subscribeToShapes: shape => diff --git a/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala index c915671164..840914cedd 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/SubstitutionGrowthTest.scala @@ -123,6 +123,26 @@ class SubstitutionGrowthTest extends AnyFunSuite: assert(m.views <= 2 * k + 4) assert(m.activations <= 2 * k + 4) + // Each spread observes both branches of a recursive iterable producer. The + // element hosts must join those flows instead of enumerating their product. + // Diff tests check the resulting members; only this test observes graph size. + test("recursive iterable spreads keep inference growth bounded"): + for k <- List(2, 6, 10, 14) do + val spreads = List.fill(k)("...xs").mkString(", ") + val m = measure(s"Iterable_$k", + s"""#lang(0.3.x) + | + |class Item(val value: Int) + |private fun forward[A](xs: Iterable[A] | Array[A], n: Int): Iterable[A] | Array[A] = + | if n == 0 then xs else forward([$spreads], n - 1) + |fun use(xs: Iterable[Item]) = [...forward(xs, 2)].map(x => x.value) + |""".stripMargin) + withClue(s"recursive iterable with $k spreads: $m"): + assert(m.largestSubstitution <= 4) + assert(m.views <= 80 * k + 300) + assert(m.activations <= 20 * k + 100) + assert(m.typeInstances <= 8 * k + 20) + /** Shapes and instances for the substitution laws below. */ private class Harness: given owner: Elaborator.State = new Elaborator.State diff --git a/hkmc2/jvm/src/test/scala/hkmc2/TestFolders.scala b/hkmc2/jvm/src/test/scala/hkmc2/TestFolders.scala index 94739a7bc3..ed109404ba 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/TestFolders.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/TestFolders.scala @@ -58,6 +58,10 @@ object TestFolders: def wasmDiffDir(wd: os.Path): os.Path = diffTestDir(wd)/"wasm" + /** Diff test subdirectories that belong to the hkmc2SlopTests project. */ + def slopDiffDir(wd: os.Path): os.Path = + diffTestDir(wd)/"slop" + /** Diff test directories that are always excluded (staging, mlscript-compile). */ def alwaysExcludedDiffDirs(wd: os.Path): Ls[os.Path] = (diffTestDir(wd)/"ucs"/"staging") :: compileTestDir(wd) :: Nil @@ -67,6 +71,7 @@ object TestFolders: nofibDiffDir(wd) :: appsDiffDir(wd) :: wasmDiffDir(wd) :: + slopDiffDir(wd) :: alwaysExcludedDiffDirs(wd) /** Check whether a file should be excluded from the given list of excluded diff --git a/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala b/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala index ead3d9cd48..8b2c31962f 100644 --- a/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala +++ b/hkmc2/jvm/src/test/scala/hkmc2/TypeRelationTest.scala @@ -42,15 +42,15 @@ class TypeRelationTest extends AnyFunSuite: val first = IntroShape(Term.UnitVal(), N) val second = DynShape() h.resolver.publishParameter(a, first) - h.resolver.constrainTypes(ar, br) - h.resolver.constrainTypes(br, ar) + h.resolver.constrainTypes(ar, br, N) + h.resolver.constrainTypes(br, ar, N) val seen = h.observe(br) h.resolver.publishParameter(a, second) assert(seen.toSet == Set(first, second)) val counts = (a.inferenceHost.listeners.size, b.inferenceHost.listeners.size) (1 to 1000).foreach: _ => - h.resolver.constrainTypes(ar, br) - h.resolver.constrainTypes(br, ar) + h.resolver.constrainTypes(ar, br, N) + h.resolver.constrainTypes(br, ar, N) h.resolver.publishParameter(a, first) assert((a.inferenceHost.listeners.size, b.inferenceHost.listeners.size) == counts) assert(seen.toList == List(first, second)) @@ -224,7 +224,7 @@ class TypeRelationTest extends AnyFunSuite: val first = IntroShape(Term.UnitVal(), N) val later = DynShape() h.resolver.publishParameter(a, first) - h.resolver.constrainTypes(left, right) + h.resolver.constrainTypes(left, right, N) val seen = h.observe(ContextualType(bt, Nil)) assert(seen.toList == List(first)) def counts = List(left.tpe.resolution.inferenceHost.listeners.size, right.tpe.resolution.inferenceHost.listeners.size, @@ -233,7 +233,7 @@ class TypeRelationTest extends AnyFunSuite: h.resolver.publishParameter(a, later) assert(seen.toList == List(first, later)) (1 to 1000).foreach: _ => - h.resolver.constrainTypes(left, right) + h.resolver.constrainTypes(left, right, N) assert(counts == before) assert(h.state.allocatedTypeInstanceCount == 0) @@ -245,7 +245,7 @@ class TypeRelationTest extends AnyFunSuite: val second = DynShape() val early = new TypeResolution(Term.UnitVal(), _ => fail("Unexpected type error")) val late = new TypeResolution(Term.UnitVal(), _ => fail("Unexpected type error")) - early.publish(TypeShape.Tuple(List(at.resolution, late))) + early.publish(TypeShape.Tuple(List(at.resolution, late).map(TypeShape.TupleField(_, spread = false)))) val left = h.resolver.declaredType(early, Map(a -> h.tpe(TypeShape.Inferred(first)))) val right = h.resolver.declaredType(early, Map(a -> h.tpe(TypeShape.Inferred(second)))) val leftSeen = h.observe(ContextualType(left, Nil)) @@ -253,7 +253,7 @@ class TypeRelationTest extends AnyFunSuite: assert(leftSeen.isEmpty && rightSeen.isEmpty) // Closing a recursive dependency must wake both observers without merging // their environments or allocating a parameter for the forward reference. - late.publish(TypeShape.Tuple(List(at.resolution, early))) + late.publish(TypeShape.Tuple(List(at.resolution, early).map(TypeShape.TupleField(_, spread = false)))) def firstField(values: ArrayBuffer[TermShape]): Set[TermShape] = values.toList match case (tuple: TupleShape) :: Nil => tuple.elements.head match case TupleShape.TypedField(tpe, marks) => h.observe(ContextualType(tpe, marks)).toSet @@ -262,10 +262,10 @@ class TypeRelationTest extends AnyFunSuite: assert(firstField(leftSeen) == Set(first)) assert(firstField(rightSeen) == Set(second)) (1 to 1000).foreach: _ => - h.resolver.constrainTypes(ContextualType(left, Nil), ContextualType(right, Nil)) + h.resolver.constrainTypes(ContextualType(left, Nil), ContextualType(right, Nil), N) val saturated = (early.inferenceHost.listeners.size, late.inferenceHost.listeners.size) (1 to 1000).foreach: _ => - h.resolver.constrainTypes(ContextualType(left, Nil), ContextualType(right, Nil)) + h.resolver.constrainTypes(ContextualType(left, Nil), ContextualType(right, Nil), N) assert((early.inferenceHost.listeners.size, late.inferenceHost.listeners.size) == saturated) assert(h.state.allocatedTypeInstanceCount == 0) @@ -285,7 +285,7 @@ class TypeRelationTest extends AnyFunSuite: val first = IntroShape(Term.UnitVal(), N) val second = DynShape() val lower = ContextualType(h.tpe(TypeShape.Inferred(first)), Nil) - h.resolver.constrainTypes(lower, neg) + h.resolver.constrainTypes(lower, neg, N) assert(negativeValues.isEmpty && positiveValues.isEmpty) resolution.publish(TypeShape.Argument(TypeArgument(inputType, outputType))) assert(negativeValues.toList == List(first)) @@ -300,7 +300,7 @@ class TypeRelationTest extends AnyFunSuite: assert(h.resolver.selectArgument(argument, true) eq positive) assert(h.resolver.selectArgument(negative, true) eq negative) assert(h.resolver.selectArgument(positive, false) eq positive) - h.resolver.constrainTypes(lower, neg) + h.resolver.constrainTypes(lower, neg, N) assert((resolution.inferenceHost.listeners.size, input.inferenceHost.listeners.size, output.inferenceHost.listeners.size) == counts) assert(negativeValues.toList == List(first)) assert(positiveValues.toList == List(second)) @@ -321,10 +321,10 @@ class TypeRelationTest extends AnyFunSuite: val value = IntroShape(Term.UnitVal(), N) h.resolver.publishParameter(parameter, value) assert(seen.toList == List(value)) - h.resolver.constrainTypes(reference, reference) + h.resolver.constrainTypes(reference, reference, N) val counts = (resolution.inferenceHost.listeners.size, parameter.inferenceHost.listeners.size) (1 to 1000).foreach: _ => - h.resolver.constrainTypes(reference, reference) + h.resolver.constrainTypes(reference, reference, N) assert(h.resolver.selectArgument(argument, true) eq selected) assert((resolution.inferenceHost.listeners.size, parameter.inferenceHost.listeners.size) == counts) assert(seen.toList == List(value)) @@ -338,21 +338,21 @@ class TypeRelationTest extends AnyFunSuite: val second = h.resolver.omittedType(h.tpe(TypeShape.Abstract).resolution, parameter) val left = ContextualType(first, Nil) val right = ContextualType(second, Nil) - h.resolver.constrainTypes(left, right) - h.resolver.constrainTypes(right, left) + h.resolver.constrainTypes(left, right, N) + h.resolver.constrainTypes(right, left, N) val seen = h.observe(right) assert(seen.isEmpty) val bound = IntroShape(Term.UnitVal(), N) val input = ContextualType(h.tpe(TypeShape.Inferred(bound)), Nil) - h.resolver.constrainTypes(input, left) + h.resolver.constrainTypes(input, left, N) def listeners(tpe: DeclaredType): Int = tpe.resolution.currentShapes.toList match case TypeShape.Hole(host) :: Nil => host.listeners.size case _ => fail("An omitted argument must retain its inference host") val counts = (listeners(first), listeners(second)) (1 to 1000).foreach: _ => - h.resolver.constrainTypes(left, right) - h.resolver.constrainTypes(right, left) - h.resolver.constrainTypes(input, left) + h.resolver.constrainTypes(left, right, N) + h.resolver.constrainTypes(right, left, N) + h.resolver.constrainTypes(input, left, N) assert(seen.toList == List(bound)) assert((listeners(first), listeners(second)) == counts) assert(h.state.allocatedTypeInstanceCount == 0) @@ -377,8 +377,8 @@ class TypeRelationTest extends AnyFunSuite: h.resolver.listenInstanceViews(h.resolver.instanceShape(hole)(using right))(rightValues += _)(using right) val first = IntroShape(Term.UnitVal(), N) val second = DynShape() - h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(first)), Nil), ContextualType(hole, Nil))(using left) - h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(second)), Nil), ContextualType(hole, Nil))(using right) + h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(first)), Nil), ContextualType(hole, Nil), N)(using left) + h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(second)), Nil), ContextualType(hole, Nil), N)(using right) assert(leftValues.toList == List(first)) assert(rightValues.toList == List(second)) assert(exporter.isEmpty) @@ -421,22 +421,22 @@ class TypeRelationTest extends AnyFunSuite: val (b, bt) = h.parameter("B") val (_, result) = h.parameter("Result") val input = ContextualType(h.tpe(TypeShape.Intersection(at.resolution, bt.resolution)), Nil) - val expected = ContextualType(h.tpe(TypeShape.Tuple(result.resolution :: Nil)), Nil) + val expected = ContextualType(h.tpe(TypeShape.Tuple(TypeShape.TupleField(result.resolution, spread = false) :: Nil)), Nil) val seen = h.observe(ContextualType(result, Nil)) def tuple(value: TermShape): TermShape = - h.resolver.instanceShape(h.tpe(TypeShape.Tuple(h.tpe(TypeShape.Inferred(value)).resolution :: Nil))) + h.resolver.instanceShape(h.tpe(TypeShape.Tuple(TypeShape.TupleField(h.tpe(TypeShape.Inferred(value)).resolution, spread = false) :: Nil))) val first = IntroShape(Term.UnitVal(), N) val second = DynShape() val left = tuple(first) val right = tuple(second) - h.resolver.constrainTypes(input, expected) + h.resolver.constrainTypes(input, expected, N) h.resolver.publishParameter(a, left) assert(seen.toList == List(first)) h.resolver.publishParameter(b, right) assert(seen.toSet == Set(first, second)) val counts = (a.inferenceHost.listeners.size, b.inferenceHost.listeners.size) (1 to 1000).foreach: _ => - h.resolver.constrainTypes(input, expected) + h.resolver.constrainTypes(input, expected, N) h.resolver.publishParameter(a, left) h.resolver.publishParameter(b, right) assert((a.inferenceHost.listeners.size, b.inferenceHost.listeners.size) == counts) @@ -452,10 +452,10 @@ class TypeRelationTest extends AnyFunSuite: val first = IntroShape(Term.UnitVal(), N) val second = DynShape() h.state.markExplicitTypeArgument(a) - h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(first)), Nil), ContextualType(at, Nil)) + h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(first)), Nil), ContextualType(at, Nil), N) h.resolver.publishParameter(a, h.resolver.instanceShape(one)) h.resolver.publishParameter(a, h.resolver.instanceShape(two)) - h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(second)), Nil), ContextualType(at, Nil)) + h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(second)), Nil), ContextualType(at, Nil), N) for target <- List(one, two); bound <- List(first, second) do assert(h.state.hasTypeConstraint(target, bound, Nil)) assert(a.currentShapes.toSet == Set(h.resolver.instanceShape(one), h.resolver.instanceShape(two))) @@ -478,7 +478,7 @@ class TypeRelationTest extends AnyFunSuite: h.state.markExplicitTypeArgument(b) h.resolver.publishParameter(a, h.resolver.instanceShape(bt).exit(sourceMarks).enter(targetMarks)) h.resolver.publishParameter(b, h.resolver.instanceShape(concrete).enter(sourceMarks)) - h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(bound)), Nil), ContextualType(at, targetMarks)) + h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(bound)), Nil), ContextualType(at, targetMarks), N) assert(h.state.hasTypeConstraint(concrete, bound, Nil)) val expected = h.resolver.instanceShape(concrete).enter(sourceMarks) match case value: TermShape => ShapeIdentity.key(value) @@ -498,7 +498,7 @@ class TypeRelationTest extends AnyFunSuite: h.state.markExplicitTypeArgument(a) h.resolver.publishParameter(a, h.resolver.instanceShape(one).enter(firstMarks)) h.resolver.publishParameter(a, h.resolver.instanceShape(two).enter(secondMarks)) - h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(bound)), Nil), ContextualType(at, firstMarks)) + h.resolver.constrainTypes(ContextualType(h.tpe(TypeShape.Inferred(bound)), Nil), ContextualType(at, firstMarks), N) assert(h.state.hasTypeConstraint(one, bound, Nil)) assert(!h.state.hasTypeConstraint(two, bound, Nil)) @@ -514,12 +514,12 @@ class TypeRelationTest extends AnyFunSuite: h.state.markExplicitTypeArgument(b) h.resolver.publishParameter(a, h.resolver.instanceShape(bt)) h.resolver.publishParameter(b, h.resolver.instanceShape(at)) - h.resolver.constrainTypes(lower, ContextualType(at, Nil)) + h.resolver.constrainTypes(lower, ContextualType(at, Nil), N) h.resolver.publishParameter(b, h.resolver.instanceShape(concrete)) assert(h.state.hasTypeConstraint(concrete, bound, Nil)) val counts = (a.inferenceHost.listeners.size, b.inferenceHost.listeners.size) (1 to 1000).foreach: _ => - h.resolver.constrainTypes(lower, ContextualType(at, Nil)) + h.resolver.constrainTypes(lower, ContextualType(at, Nil), N) h.resolver.publishParameter(a, h.resolver.instanceShape(bt)) h.resolver.publishParameter(b, h.resolver.instanceShape(at)) assert((a.inferenceHost.listeners.size, b.inferenceHost.listeners.size) == counts) @@ -544,8 +544,8 @@ class TypeRelationTest extends AnyFunSuite: val (b, bt) = h.parameter("B") val ar = ContextualType(at, Nil) val br = ContextualType(bt, Nil) - h.resolver.constrainTypes(ar, br) - h.resolver.constrainTypes(br, ar) + h.resolver.constrainTypes(ar, br, N) + h.resolver.constrainTypes(br, ar, N) val output = new TermShapeHost h.resolver.listenInstanceViews(h.resolver.instanceShape(bt))(output.publish) val exporter = ArrayBuffer.empty[TermShape] diff --git a/hkmc2/shared/src/main/scala/hkmc2/AsyncLowering.scala b/hkmc2/shared/src/main/scala/hkmc2/AsyncLowering.scala index 9118221ce6..04d79ceb1f 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/AsyncLowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/AsyncLowering.scala @@ -65,7 +65,7 @@ class AsyncLowering(using TL, Raise, Elaborator.State, Elaborator.Ctx, Config): k(Value.SimpleRef(thisVar)(v.toLoc)) case _ => super.applyValue(v)(k) val newBody = transformer.applyBlock(wrapAwait(true)(applyFunBodyLikeBlock(fun.body))) - collectedFunDefn += FunDefn(N, outerBms, outerDsym, PlainParamList((thisParam.iterator ++ outerParams.iterator.map(_._2)).toList)(N) :: PlainParamList(Nil)(N) :: Nil, newBody)(fun.configOverride, noAsync) + collectedFunDefn += FunDefn(N, outerBms, outerDsym, LoweredParamList.plain((thisParam.iterator ++ outerParams.iterator.map(_._2)).toList)(N) :: LoweredParamList.plain(Nil)(N) :: Nil, newBody)(fun.configOverride, noAsync) val callArgs = (fun.owner.iterator.map(s => Arg(N, Value.This(s)(N))) ++ fun.params.iterator.flatMap(_.allParams.iterator.map(p => Arg(N, Value.SimpleRef(p.sym)(N))))).toList val outerCall = Call(Value.MemberRef(outerBms, outerDsym)(N), callArgs ne_:: Nil)(CallMetadata.mlsFunWithEffect, N) val tmp = TempSymbol(N, erasedType = outerCall.erasedValueType, "tmp") diff --git a/hkmc2/shared/src/main/scala/hkmc2/Config.scala b/hkmc2/shared/src/main/scala/hkmc2/Config.scala index f05867d244..a28640401b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/Config.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/Config.scala @@ -137,7 +137,7 @@ object Config: useNewResolution: Bool, strictResolution: Bool, // Reject multiple resolved selection targets as well as missing targets // Unknown inputs admitted by a public interface are checked regardless of this flag. - eagerResolution: Bool, // Reject selections whose prefixes have shapes that do not support them, even though the selection might have other valid targets + eagerResolution: Bool, // Reject invalid selection/call candidates even when other candidates supply valid targets typeCheck: Opt[TypeChecking], )( // * `N` when no version was specified with `#lang` (see `Language.default`), diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala index f6fa2f6623..96ff611e73 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Block.scala @@ -121,8 +121,10 @@ sealed abstract class Block extends Product: case AssignField(lhs, nme, rhs, rest) => s"""|AssignField(${lhs.showDbg}.${nme} = ${rhs.showDbg}) |${rest.showDbg}""".stripMargin - case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => - val access = if arrayIdx then s"[${fld.showDbg}]" else s".${fld.showDbg}" + case AssignDynField(lhs, fld, kind, rhs, rest) => + val access = kind match + case DynSelectKind.ArrayIdx => s"[${fld.showDbg}]" + case DynSelectKind.Member(_) | DynSelectKind.Unknown => s".${fld.showDbg}" s"""|AssignDynField(${lhs.showDbg}$access = ${rhs.showDbg}) |${rest.showDbg}""".stripMargin case Define(defn, rest) => @@ -213,7 +215,7 @@ sealed abstract class Block extends Product: case Assign(NoSymbol, rhs, rest) => rhs.freeVars ++ rest.freeVars case Assign(lhs: LocalVarSymbol, rhs, rest) => Set.single(lhs) ++ rhs.freeVars ++ rest.freeVars case AssignField(lhs, nme, rhs, rest) => lhs.freeVars ++ rhs.freeVars ++ rest.freeVars - case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => lhs.freeVars ++ fld.freeVars ++ rhs.freeVars ++ rest.freeVars + case AssignDynField(lhs, fld, kind, rhs, rest) => lhs.freeVars ++ fld.freeVars ++ rhs.freeVars ++ rest.freeVars case Define(defn, rest) => defn.freeVars ++ rest.freeVars case Scoped(syms, body) => body.freeVars -- syms case End(msg) => Set.empty @@ -314,11 +316,11 @@ sealed abstract class Block extends Product: then this else AssignField(lhs, nme, rhs, newRest)(a.symbol) - case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => + case AssignDynField(lhs, fld, kind, rhs, rest) => val newRest = rest.flatten(k) if newRest is rest then this - else AssignDynField(lhs, fld, arrayIdx, rhs, newRest) + else AssignDynField(lhs, fld, kind, rhs, newRest) case Define(defn, rest) => val newDefn = defn match @@ -437,7 +439,7 @@ case class Assign(lhs: Assignable, rhs: Result, rest: Block) extends Block with case class AssignField(lhs: Path, nme: Tree.Ident, rhs: Result, rest: Block)(val symbol: Opt[MemberSymbol]) extends Block with ProductWithTail with NonBlockTail -case class AssignDynField(lhs: Path, fld: Path, arrayIdx: Bool, rhs: Result, rest: Block) +case class AssignDynField(lhs: Path, fld: Path, kind: DynSelectKind, rhs: Result, rest: Block) extends Block with ProductWithTail with NonBlockTail case class Define(defn: Defn, rest: Block) extends Block with ProductWithTail with NonBlockTail: @@ -492,9 +494,9 @@ object AssignField: case Scoped(syms, body) => Scoped(syms, AssignField(lhs, nme, rhs, body)(symbol)) case _ => new AssignField(lhs, nme, rhs, rest)(symbol) object AssignDynField: - def apply(lhs: Path, fld: Path, arrayIdx: Bool, rhs: Result, rest: Block): Block = rest match - case Scoped(syms, body) => Scoped(syms, AssignDynField(lhs, fld, arrayIdx, rhs, body)) - case _ => new AssignDynField(lhs, fld, arrayIdx, rhs, rest) + def apply(lhs: Path, fld: Path, kind: DynSelectKind, rhs: Result, rest: Block): Block = rest match + case Scoped(syms, body) => Scoped(syms, AssignDynField(lhs, fld, kind, rhs, body)) + case _ => new AssignDynField(lhs, fld, kind, rhs, rest) object Define: def apply(defn: Defn, rest: Block): Block = rest match case Scoped(syms, body) => Scoped(syms, Define(defn, body)) @@ -585,12 +587,12 @@ object HandleBlock: )(using Elaborator.State, Elaborator.Ctx) = val sym = new BlockMemberSymbol("handleBlock$", Nil, false) - val bodyDefn = FunDefn.withFreshSymbol(N, sym, PlainParamList(Nil)(N) :: Nil, body)(N, annotations = Nil) + val bodyDefn = FunDefn.withFreshSymbol(N, sym, LoweredParamList.plain(Nil)(N) :: Nil, body)(N, annotations = Nil) val handlerMtds = handlers.map: handler => val sym = BlockMemberSymbol(cls.nme + handler.sym.nme, Nil, true) val fDef = FunDefn.withFreshSymbol( - N, sym, PlainParamList(Param.simple(handler.resumeSym) :: Nil)(N) :: Nil, + N, sym, LoweredParamList.plain(Param.simple(handler.resumeSym) :: Nil)(N) :: Nil, handler.body )(N, annotations = Nil) val rSym = TempSymbol(N, erasedType = N, "suspendRes") @@ -616,6 +618,7 @@ object HandleBlock: N, N, N, + Map.empty, )(N, Nil) blockBuilder @@ -667,12 +670,14 @@ sealed abstract class Defn: // * Simple heuristic. TODO: check the purity of the ctor somehow? (ignore pure local field inits) c.companion.isEmpty && (!(c.k is syntax.Obj) || c.ctor.isEmpty) + && c.methodKeys.valuesIterator.forall(_.isPure) def subBlocks: Ls[Block] = this match case FunDefn(body = body) => body :: Nil case _: ValDefn => Nil - case ClsLikeDefn(preCtor = preCtor, ctor = ctor, methods = mtds, companion = comp) => - preCtor :: ctor :: mtds.flatMap(_.subBlocks) ::: comp.toList.flatMap(_.subBlocks) + case cls @ ClsLikeDefn(preCtor = preCtor, ctor = ctor, methods = mtds, companion = comp) => + preCtor :: ctor :: mtds.flatMap(_.subBlocks) ::: comp.toList.flatMap(_.subBlocks) ::: + cls.methodKeys.valuesIterator.flatMap(_.subBlocks).toList // * Note that `privateFields` abd `publicFields` can't possibly be free since they are never // * referred to directly (they are only accessed through selections). @@ -684,27 +689,28 @@ sealed abstract class Defn: // * Value definitions without an owner are similar to local variables; // * they need to be declared with `let` in JS, which is why we add them here. vd.sym.optionIf(tsym.owner.isEmpty) - case ClsLikeDefn(own, isym, sym, ctorSym, k, paramsOpt, auxParams, parentSym, - methods, privateFields, publicFields, preCtor, ctor, stat, bufferable, termCompanion) => + case cls @ ClsLikeDefn(own, isym, sym, ctorSym, k, paramsOpt, auxParams, parentSym, + methods, privateFields, publicFields, preCtor, ctor, stat, bufferable, termCompanion, _) => preCtor.freeVars ++ ctor.freeVars ++ methods.flatMap(_.freeVars) -- auxParams.flatMap(_.paramSyms) ++ stat.iterator.flatMap(_.freeVars) ++ sym.optionIf(own.isEmpty) ++ parentSym.iterator.flatMap(_.freeVars) + ++ cls.methodKeys.valuesIterator.flatMap(_.freeVars) lazy val size: Int = this match case FunDefn(_, _, _, params, body) => 1 + body.size case ValDefn(_, _, rhs) => 1 - case ClsLikeDefn(_, _, _, _, _, paramsOpt, auxParams, parentSym, methods, privateFields, publicFields, preCtor, ctor, stat, bufferable, termCompanion) => - 1 + methods.map(_.size).sum + preCtor.size + ctor.size + stat.fold(0)(_.size) + case cls @ ClsLikeDefn(_, _, _, _, _, paramsOpt, auxParams, parentSym, methods, privateFields, publicFields, preCtor, ctor, stat, bufferable, termCompanion, _) => + 1 + methods.map(_.size).sum + preCtor.size + ctor.size + stat.fold(0)(_.size) + cls.methodKeys.valuesIterator.map(_.size).sum final case class FunDefn( owner: Opt[InnerSymbol], sym: BlockMemberSymbol, dSym: TermSymbol, - params: Ls[ParamList], + params: Ls[LoweredParamList], body: Block, )( val configOverride: Opt[Config], @@ -799,11 +805,13 @@ private[codegen] object InlinerBodySummary: recordFunction(fun, false) case cls: ClsLikeDefn => cls.parentPath.foreach(applyPath) + cls.methodKeys.valuesIterator.foreach(applyPath) cls.methods.foreach(recordFunction(_, true)) withoutCallGraphSource: applySubBlock(cls.preCtor) applySubBlock(cls.ctor) cls.companion.foreach: module => + module.methodKeys.valuesIterator.foreach(applyPath) module.methods.foreach(recordFunction(_, true)) // The module constructor is run with the enclosing constructor and therefore inherits // whether its surrounding block has a call-graph source. @@ -821,7 +829,7 @@ private[codegen] object InlinerBodySummary: InlinerBodySummary(entries.toList, transitiveCallTargets) object FunDefn: - def withFreshSymbol(owner: Opt[InnerSymbol], sym: BlockMemberSymbol, params: Ls[ParamList], body: Block)(configOverride: Opt[Config], annotations: Ls[Annot])(using State) = + 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) sym.tsym = S(tSym) FunDefn(owner, sym, tSym, params, body)(configOverride, annotations) @@ -913,8 +921,8 @@ final case class ClsLikeDefn( sym: BlockMemberSymbol, ctorSym: Opt[ClassCtorSymbol], k: syntax.ClsLikeKind, - paramsOpt: Opt[ParamList], - auxParams: List[ParamList], + paramsOpt: Opt[LoweredParamList], + auxParams: List[LoweredParamList], parentPath: Opt[Path], methods: Ls[FunDefn], privateFields: Ls[TermSymbol], @@ -924,6 +932,8 @@ final case class ClsLikeDefn( companion: Opt[ClsLikeBody], bufferable: Option[Bool], termCompanion: Opt[TermSymbol], + // Computed names are structural operands evaluated in the class's enclosing scope. + methodKeys: Map[TermSymbol, Path], )( val configOverride: Opt[Config], val annotations: Ls[Annot], @@ -946,6 +956,11 @@ final case class ClsLikeDefn( c.methods.foreach: m => m.dSym.irDefn = S(m) val defnSym = S(isym) + override def equals(obj: Any): Bool = obj match + case that: ClsLikeDefn => productIterator.sameElements(that.productIterator) && + configOverride == that.configOverride && annotations == that.annotations + case _ => false + override def hashCode: Int = (productIterator.toList, configOverride, annotations).hashCode // * This is only supposed to be for companion module definitions (notably, not for `object`) @@ -956,16 +971,17 @@ final case class ClsLikeBody( publicFields: Ls[BlockMemberSymbol -> TermSymbol], ctor: Block, annotations: Ls[Annot], + methodKeys: Map[TermSymbol, Path], ): lazy val visibility: Visibility = Visibility.of(annotations) def isStaged: Bool = annotations.exists: case Annot.Modifier(Keyword.`staged`) => true case _ => false def subBlocks: Ls[Block] = - ctor :: methods.flatMap(_.subBlocks) + ctor :: methods.flatMap(_.subBlocks) ::: methodKeys.valuesIterator.flatMap(_.subBlocks).toList lazy val freeVars: Set[FreeSymbol] = - ctor.freeVars ++ methods.flatMap(_.freeVars) - lazy val size = 1 + methods.map(_.size).sum + ctor.size + ctor.freeVars ++ methods.flatMap(_.freeVars) ++ methodKeys.valuesIterator.flatMap(_.freeVars) + lazy val size = 1 + methods.map(_.size).sum + ctor.size + methodKeys.valuesIterator.map(_.size).sum /* object ClsLikeBody: @@ -982,7 +998,7 @@ object ClsLikeBody: final case class Handler( sym: BlockMemberSymbol, resumeSym: VarSymbol, - params: Ls[ParamList], + params: Ls[LoweredParamList], body: Block, ): lazy val freeVars: Set[FreeSymbol] = body.freeVars -- params.flatMap(_.paramSyms) - sym - resumeSym @@ -1036,7 +1052,7 @@ sealed abstract class Result extends Located, HasErasedType: case Value.Lit(lit) => lit.idStr case Select(q, n) => s"Select(${q.showDbg}, ${n.showDbg})" case SuperSelect(q, base, n) => s"SuperSelect(${q.showDbg}, ${base.showDbg}, ${n.showDbg})" - case DynSelect(q, fld, arrayIdx) => s"DynSelect(${q.showDbg}, ${fld.showDbg}, $arrayIdx)" + case DynSelect(q, fld, kind) => s"DynSelect(${q.showDbg}, ${fld.showDbg}, ${kind.showDbg})" case Call(fun, argss) => s"Call(${fun.showDbg}, [${ argss.map(_.map(a => a.value.showDbg).mkString("[", ", ", "]")).mkString(", ")}])" case Lambda(params, body) => s"Lambda(${params.showDbg}, ${body.showDbg})" @@ -1064,7 +1080,8 @@ sealed abstract class Result extends Located, HasErasedType: case _: Value => true case sel @ Select(q, n) => q.isPure && sel.symbol.exists(_.isPure) - case sel @ SuperSelect(q, base, _) => q.isPure && base.isPure && sel.symbol.isPure + case DynSelect(q, fld, DynSelectKind.Member(sym)) => q.isPure && fld.isPure && sym.isPure + case sel @ SuperSelect(q, base, key) => q.isPure && base.isPure && key.isPure && sel.symbol.isPure case c @ Call(fun, ass) if c.isKnownUnsaturatedCall => fun.isPure && ass.forall(_.forall(a => a.spread.isEmpty && a.value.isPure)) case Call(Value.SimpleRef(bs: BuiltinSymbol), ass) if bs.isPure => @@ -1081,7 +1098,8 @@ sealed abstract class Result extends Located, HasErasedType: case Call(fun, argss) => fun.subBlocks ::: argss.flatten.flatMap(_.value.subBlocks) case Instantiate(mut, cls, argss) => argss.flatten.flatMap(_.value.subBlocks) case Select(qual, name) => qual.subBlocks - case SuperSelect(qual, base, _) => qual.subBlocks ::: base.subBlocks + case DynSelect(qual, fld, _) => qual.subBlocks ::: fld.subBlocks + case SuperSelect(qual, base, key) => qual.subBlocks ::: base.subBlocks ::: key.subBlocks case Lambda(params, body) => body :: Nil case Tuple(mut, elems) => elems.flatMap(_.value.subBlocks) case _ => Nil @@ -1091,7 +1109,7 @@ sealed abstract class Result extends Located, HasErasedType: case Instantiate(mut, cls, argss) => cls.freeVars ++ argss.iterator.flatten.flatMap(_.value.freeVars).toSet case Cast(value, _, _) => value.freeVars case Select(qual, name) => qual.freeVars - case SuperSelect(qual, base, _) => qual.freeVars ++ base.freeVars + case SuperSelect(qual, base, key) => qual.freeVars ++ base.freeVars ++ key.freeVars case Lambda(params, body) => body.freeVars -- params.paramSyms case Tuple(mut, elems) => elems.iterator.flatMap(_.value.freeVars).toSet case Record(mut, args) => @@ -1100,21 +1118,21 @@ sealed abstract class Result extends Located, HasErasedType: case Value.MemberRef(bms, disamb) => Set.single(bms) case Value.This(sym) => Set.empty case Value.Lit(lit) => Set.empty - case DynSelect(qual, fld, arrayIdx) => qual.freeVars ++ fld.freeVars + case DynSelect(qual, fld, _) => qual.freeVars ++ fld.freeVars lazy val size: Int = this match case Call(fun, argss) => fun.size + argss.iterator.flatten.map(_.value.size).sum case Instantiate(mut, cls, argss) => cls.size + argss.iterator.flatten.map(_.value.size).sum case Cast(value, _, _) => value.size case Select(qual, name) => qual.size - case SuperSelect(qual, base, _) => qual.size + base.size + case SuperSelect(qual, base, key) => qual.size + base.size + key.size case Lambda(params, body) => 1 + body.size case Tuple(mut, elems) => elems.iterator.map(_.value.size).sum case Record(mut, args) => args.iterator.map(arg => arg.idx.fold(0)(_.size) + arg.value.size).sum case _: Value.RefLike => 0 case Value.Lit(l: Tree.StrLit) => l.value.length / 4 case Value.Lit(lit) => 0 - case DynSelect(qual, fld, arrayIdx) => qual.size + fld.size + case DynSelect(qual, fld, _) => qual.size + fld.size lazy val erasedType: Opt[ErasedType] = this match case Value.SimpleRef(sym) => sym match @@ -1154,6 +1172,7 @@ sealed abstract class Result extends Located, HasErasedType: // * A resolved selection has the type of the member it refers to (e.g. `this.field`); an // * unresolved selection (dynamic field access) stays unknown. case sel: SuperSelect => sel.symbol.erasedType + case DynSelect(_, _, DynSelectKind.Member(sym)) => sym.erasedType case sel @ Select(_, _) => sel.symbol match case S(ts: TermSymbol) => ts.erasedType // * A class reference is the class object, so it stays unknown. @@ -1335,7 +1354,7 @@ object Cast: case Cast(inner, _, innerCheck) => new Cast(inner, target, check || innerCheck)(toLoc) case _ => new Cast(value, target, check)(toLoc) -case class Lambda(params: ParamList, body: Block)(val annot: Ls[Annot], val toLoc: Opt[Loc]) extends Result: +case class Lambda(params: LoweredParamList, body: Block)(val annot: Ls[Annot], val toLoc: Opt[Loc]) extends Result: def withLoc(loc: Opt[Loc]): Lambda = if loc == toLoc then this else copy()(annot, loc) lazy val affine: Bool = annot.exists(_.isInstanceOf[Annot.Affine]) @@ -1358,6 +1377,7 @@ sealed abstract class Path extends TrivialResult: case ref: Value.MemberRef => S(ref.disamb) case sel: Select => sel.symbol case sel: SuperSelect => S(sel.symbol) + case DynSelect(_, _, DynSelectKind.Member(sym)) => S(sym) case _ => N /** @@ -1372,12 +1392,36 @@ case class Select(qual: Path, name: Tree.Ident)(val symbol: Opt[DefinitionSymbol * reference or a previously evaluated value. No extends expression is re-evaluated at a * super call. Both operands are explicit so lifting and inlining preserve their bindings. */ -case class SuperSelect(qual: Path, base: Path, name: Tree.Ident)(val symbol: TermSymbol, val toLoc: Opt[Loc]) extends Path: +case class SuperSelect(qual: Path, base: Path, key: Path)(val symbol: TermSymbol, val toLoc: Opt[Loc]) extends Path: def withLoc(loc: Opt[Loc]): SuperSelect = if loc == toLoc then this else copy()(symbol, loc) -case class DynSelect(qual: Path, fld: Path, arrayIdx: Bool)(val toLoc: Opt[Loc]) extends Path: +/** A selection whose field is computed at runtime by the `fld` operand. + * `kind` says how `fld` is interpreted, and whether resolution knows the selected member. + */ +case class DynSelect(qual: Path, fld: Path, kind: DynSelectKind)(val toLoc: Opt[Loc]) extends Path: def withLoc(loc: Opt[Loc]): DynSelect = if loc == toLoc then this else copy()(loc) +/** How the computed field operand of a [[DynSelect]] or an [[AssignDynField]] is interpreted. */ +enum DynSelectKind: + /** An integer index into an array-like value. When reading, a negative index counts from the end, + * as with JavaScript's `Array.prototype.at`. */ + case ArrayIdx + /** The runtime key of the statically resolved member `symbol`, such as a method declared with + * `@jsSymbol(key)`. The key is an ordinary operand rather than a name, so that captures, imports, + * substitution and lifting see its dependencies. Passes that reason about resolved members + * (purity, erased types, call targets, flow analysis) treat it like a resolved [[Select]]. */ + case Member(symbol: TermSymbol) + /** A property key not resolved to any known member. */ + case Unknown + + def isArrayIdx: Bool = this match + case ArrayIdx => true + case Member(_) | Unknown => false + + def showDbg(using DebugPrinter): Str = this match + case Member(sym) => s"Member(${sym.showDbg})" + case ArrayIdx | Unknown => toString + enum Value extends Path with ProductWithExtraInfo: case SimpleRef(sym: SimpleSymbol)(val toLoc: Opt[Loc]) /** @@ -1447,6 +1491,7 @@ object TermSymbolPath: case S(sym: TermSymbol) => S(sym) case _ => N case selection: SuperSelect => S(selection.symbol) + case DynSelect(_, _, DynSelectKind.Member(sym)) => S(sym) case _ => N case class Arg(spread: Opt[SpreadKind], value: Path) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockChecker.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockChecker.scala index 3e5df6ed37..6a198acb54 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockChecker.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockChecker.scala @@ -35,7 +35,7 @@ class BlockChecker()(using DebugPrinter, State, Raise) extends BlockTraverser: case _ => () super.applyBlock(b) - override def applyParamList(pl: ParamList): Unit = + override def applyParamList(pl: LoweredParamList): Unit = pl.paramSyms.foreach(checkSymbol(_, pl)) super.applyParamList(pl) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala index 25389d7c2c..8b8944ffcf 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockSimplifier.scala @@ -126,7 +126,7 @@ class BlockSimplifier def localVars(block: Block): Set[LocalVar] = val locals: MutSet[LocalVar] = MutSet.empty[LocalVar] - def paramsOf(paramLists: IterableOnce[ParamList]): Unit = + def paramsOf(paramLists: IterableOnce[LoweredParamList]): Unit = locals ++= paramLists.iterator.flatMap(_.paramSyms) def rec(current: Block): Unit = current match @@ -1423,7 +1423,7 @@ class BlockSimplifier def register(sym: ScopedSymbol | TermSymbol | LabelSymbol | ClassCtorSymbol): Unit = if seen(sym) then found = true else seen += sym - def registerParamList(pl: ParamList): Unit = + def registerParamList(pl: LoweredParamList): Unit = pl.params.foreach(param => register(param.sym)) pl.restParam.foreach(param => register(param.sym)) (new BlockTraverser: @@ -1456,7 +1456,7 @@ class BlockSimplifier ).applyFunDefn(fun) found - def matchArgs(args: List[Arg], params: ParamList): Option[List[(VarSymbol, Result)]] = + def matchArgs(args: List[Arg], params: LoweredParamList): Option[List[(VarSymbol, Result)]] = if args.exists(_.spread.isDefined) then // we require a precise match when any arg is a spread arg if params.restParam.isEmpty then return N @@ -1468,12 +1468,14 @@ class BlockSimplifier else // otherwise arg list is a simple list, and // we can perform manual array instantiation if params contain a spread param + val paddedArgs = OptionalParams.pad(params.params, args) match + case S(args) => args + case N => return N if params.restParam.isEmpty then - if args.size =/= params.params.size then return N - S(args.zip(params.params).map((arg, param) => (param.sym, arg.value))) + if paddedArgs.size =/= params.params.size then return N + S(paddedArgs.zip(params.params).map((arg, param) => (param.sym, arg.value))) else - if args.size < params.params.size then return N - val (fixedArgs, restArgs) = args.splitAt(params.params.size) + val (fixedArgs, restArgs) = paddedArgs.splitAt(params.params.size) S(fixedArgs.zip(params.params).map((arg, param) => (param.sym, arg.value)) ++ List((params.restParam.get.sym, Tuple(true, restArgs)(N)))) @@ -1482,7 +1484,7 @@ class BlockSimplifier * or if there are fewer arg lists than param lists (partial application). * Extra arg lists beyond the param lists are ignored here and should be * handled by the caller (e.g., applied as a chained call on the result). */ - def matchAllArgs(argss: List[List[Arg]], params: List[ParamList]): Option[List[(VarSymbol, Result)]] = + def matchAllArgs(argss: List[List[Arg]], params: List[LoweredParamList]): Option[List[(VarSymbol, Result)]] = if argss.length < params.length then return N val matchedPairs = argss.zip(params) val allMatched = matchedPairs.foldLeft[Option[List[(VarSymbol, Result)]]](S(Nil)): @@ -1715,7 +1717,8 @@ class BlockSimplifier // receiver, as in `(x => x).apply(...)`, is not dead. fun match case Select(qual, _) => applyPath(qual) - case SuperSelect(qual, base, _) => applyPath(qual); applyPath(base) + case SuperSelect(qual, base, key) => applyPath(qual); applyPath(base); applyPath(key) + case DynSelect(qual, key, _) => applyPath(qual); applyPath(key) // Otherwise, `fun` is a direct `Value.MemberRef` to `ts`: the call target itself, // which `observeCall` records. Traversing it as a value would count `ts` again and // set `disallowElimination`, preventing every directly called function from being inlined. diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala index 2dacca8641..23e151a6df 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTransformer.scala @@ -95,14 +95,15 @@ class BlockTransformer(subst: SymbolSubst): applyDefn(defn): defn2 => val rst2 = applySubBlock(rst) if (defn2 is defn) && (rst2 is rst) then b else Define(defn2, rst2) - case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => + case AssignDynField(lhs, fld, kind, rhs, rest) => applyResult(rhs): rhs2 => applyPath(lhs): lhs2 => applyPath(fld): fld2 => + val kind2 = applyDynSelectKind(kind) val rest2 = applySubBlock(rest) - if (lhs2 is lhs) && (fld2 is fld) && (rhs2 is rhs) && (rest2 is rest) + if (lhs2 is lhs) && (fld2 is fld) && (kind2 is kind) && (rhs2 is rhs) && (rest2 is rest) then b - else AssignDynField(lhs2, fld2, arrayIdx, rhs2, rest2) + else AssignDynField(lhs2, fld2, kind2, rhs2, rest2) case _: Scoped => applyScopedBlock(b) // FunDefn body, Lambda body, Handler body, ctor and pCtor are considered "funBodyLike" @@ -180,16 +181,18 @@ class BlockTransformer(subst: SymbolSubst): case p: Path => applyPath(p)(k) def applyPath(p: Path)(k: Path => Block): Block = p match - case p @ SuperSelect(qual, base, name) => + case p @ SuperSelect(qual, base, key) => applyPath(qual): qual2 => applyPath(base): base2 => - val sym2 = p.symbol.subst - k(if (qual2 is qual) && (base2 is base) && (sym2 is p.symbol) then p - else SuperSelect(qual2, base2, name)(sym2, p.toLoc)) - case DynSelect(qual, fld, arrayIdx) => + applyPath(key): key2 => + val sym2 = p.symbol.subst + k(if (qual2 is qual) && (base2 is base) && (key2 is key) && (sym2 is p.symbol) then p + else SuperSelect(qual2, base2, key2)(sym2, p.toLoc)) + case DynSelect(qual, fld, kind) => applyPath(qual): qual2 => applyPath(fld): fld2 => - k(if (qual2 is qual) && (fld2 is fld) then p else DynSelect(qual2, fld2, arrayIdx)(p.toLoc)) + val kind2 = applyDynSelectKind(kind) + k(if (qual2 is qual) && (fld2 is fld) && (kind2 is kind) then p else DynSelect(qual2, fld2, kind2)(p.toLoc)) case p @ Select(qual, name) => applyPath(qual): qual2 => val sym2 = p.symbol.mapConserve(_.subst) @@ -212,6 +215,13 @@ class BlockTransformer(subst: SymbolSubst): k(if (sym2 is sym) then v else sym2.asThis.withLocOf(v)) case Value.Lit(lit) => k(v) + /** Substitutes the resolved member of a keyed selection, as for the symbol of a [[Select]]. */ + def applyDynSelectKind(kind: DynSelectKind): DynSelectKind = kind match + case DynSelectKind.Member(sym) => + val sym2 = sym.subst + if sym2 is sym then kind else DynSelectKind.Member(sym2) + case DynSelectKind.ArrayIdx | DynSelectKind.Unknown => kind + def applySimpleSymbol(sym: SimpleSymbol): SimpleSymbol = sym match case sym: LocalVarSymbol => sym.subst case sym: BuiltinSymbol => sym.subst @@ -258,11 +268,19 @@ class BlockTransformer(subst: SymbolSubst): (privateFields2 is defn.privateFields) && (publicFields2 is defn.publicFields) && (ctor2 is defn.ctor) - then defn else ClsLikeBody(isym2, methods2, privateFields2, publicFields2, ctor2, defn.annotations) + then defn else ClsLikeBody(isym2, methods2, privateFields2, publicFields2, ctor2, defn.annotations, defn.methodKeys) + + private def applyMethodKeys(keys: Map[TermSymbol, Path])(k: Map[TermSymbol, Path] => Block): Block = + def loop(todo: Ls[(TermSymbol, Path)], acc: Map[TermSymbol, Path], changed: Bool): Block = todo match + case Nil => k(if changed then acc else keys) + case (sym, key) :: rest => applyPath(key): key2 => + val sym2 = sym.subst + loop(rest, acc + (sym2 -> key2), changed || (sym2 isnt sym) || (key2 isnt key)) + loop(keys.toList, Map.empty, false) def applyClsLikeDefn(defn: ClsLikeDefn)(k: Defn => Block): Block = val ClsLikeDefn(own, isym, sym, ctorSym, kind, paramsOpt, auxParams, parentPath, methods, - privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion) = defn + privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion, _) = defn val own2 = own.mapConserve(_.subst) val isym2 = isym.subst val sym2 = sym.subst @@ -270,13 +288,18 @@ class BlockTransformer(subst: SymbolSubst): val termCompanion2 = termCompanion.mapConserve(_.subst) val paramsOpt2 = paramsOpt.mapConserve(applyParamList) val auxParams2 = auxParams.mapConserve(applyParamList) - def helper(parentPath2: Opt[Path]) = + def helper(parentPath2: Opt[Path]): Block = applyMethodKeys(defn.methodKeys): keys2 => + applyMethodKeys(mod.fold(Map.empty[TermSymbol, Path])(_.methodKeys)): modKeys2 => + finish(parentPath2, keys2, modKeys2) + def finish(parentPath2: Opt[Path], keys2: Map[TermSymbol, Path], modKeys2: Map[TermSymbol, Path]): Block = val methods2 = methods.mapConserve(applyFunDefn) val privateFields2 = privateFields.mapConserve(_.subst) val publicFields2 = publicFields.mapConserve(applyPublicField) val preCtor2 = applyFunBodyLikeBlock(preCtor) val ctor2 = applyFunBodyLikeBlock(ctor) - val mod2 = mod.mapConserve(applyObjBody) + val mod2 = mod.mapConserve: body => + val body2 = applyObjBody(body) + if modKeys2 is body2.methodKeys then body2 else body2.copy(methodKeys = modKeys2) k: if (own2 is own) && (isym2 is isym) && (sym2 is sym) && (ctorSym2 is ctorSym) && (paramsOpt2 is paramsOpt) && @@ -286,9 +309,9 @@ class BlockTransformer(subst: SymbolSubst): (privateFields2 is privateFields) && (publicFields2 is publicFields) && (preCtor2 is preCtor) && (ctor2 is ctor) && - (mod2 is mod) && (termCompanion2 is termCompanion) + (mod2 is mod) && (termCompanion2 is termCompanion) && (keys2 is defn.methodKeys) then defn else ClsLikeDefn(own2, isym2, sym2, ctorSym2, kind, paramsOpt2, - auxParams2, parentPath2, methods2, privateFields2, publicFields2, preCtor2, ctor2, mod2, bufferable, termCompanion2)(defn.configOverride, defn.annotations) + auxParams2, parentPath2, methods2, privateFields2, publicFields2, preCtor2, ctor2, mod2, bufferable, termCompanion2, keys2)(defn.configOverride, defn.annotations) parentPath match case Some(pp) => applyPath(pp): pp2 => helper: @@ -304,14 +327,14 @@ class BlockTransformer(subst: SymbolSubst): applyPath(arg.value): val2 => k(if val2 is arg.value then arg else Arg(arg.spread, val2)) - def applyParamList(pl: ParamList): ParamList = + def applyParamList(pl: LoweredParamList): LoweredParamList = def applyParam(p: Param): Param = val sym2 = p.sym.subst if sym2 is p.sym then p else p.copy(sym = sym2)(p.toLoc) val params2 = pl.params.mapConserve(applyParam) - val rest2 = pl.restParam.mapConserve(applyParam) - if (params2 is pl.params) && (rest2 is pl.restParam) - then pl else ParamList(pl.flags, params2, rest2)(pl.toLoc) + val restParam2 = pl.restParam.mapConserve(applyParam) + if (params2 is pl.params) && (restParam2 is pl.restParam) + then pl else LoweredParamList(pl.flags, params2, restParam2)(pl.toLoc) def applyCase(cse: Case)(k: Case => Block): Block = cse match case Case.Lit(lit) => k(cse) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala index dea0cb6888..f1c54d431a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/BlockTraverser.scala @@ -51,10 +51,11 @@ class BlockTraverser: case b @ AssignField(l, n, r, rst) => applyPath(l); applyResult(r); applySubBlock(rst); b.symbol.foreach(_.traverse) case Define(defn, rst) => applyDefn(defn); applySubBlock(rst) - case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => + case AssignDynField(lhs, fld, kind, rhs, rest) => applyPath(lhs) applyResult(rhs) applyPath(fld) + applyDynSelectKind(kind) applySubBlock(rest) case Scoped(_, body) => applySubBlock(body) @@ -69,10 +70,10 @@ class BlockTraverser: case p: Path => applyPath(p) def applyPath(p: Path): Unit = p match - case p @ SuperSelect(qual, base, _) => - applyPath(qual); applyPath(base); p.symbol.traverse - case DynSelect(qual, fld, arrayIdx) => - applyPath(qual); applyPath(fld) + case p @ SuperSelect(qual, base, key) => + applyPath(qual); applyPath(base); applyPath(key); p.symbol.traverse + case DynSelect(qual, fld, kind) => + applyPath(qual); applyPath(fld); applyDynSelectKind(kind) case p @ Select(qual, name) => applyPath(qual); p.symbol.foreach(_.traverse) case c @ Cast(value, _, _) => applyResult(value) @@ -86,6 +87,10 @@ class BlockTraverser: case Value.This(sym) => sym.traverse case Value.Lit(lit) => () + def applyDynSelectKind(kind: DynSelectKind): Unit = kind match + case DynSelectKind.Member(sym) => sym.traverse + case DynSelectKind.ArrayIdx | DynSelectKind.Unknown => () + def applyFunDefn(fun: FunDefn): Unit = fun.owner.foreach(_.traverse) fun.sym.traverse @@ -99,7 +104,7 @@ class BlockTraverser: def applyClsLikeDefn(defn: ClsLikeDefn): Unit = val ClsLikeDefn(own, isym, sym, ctorSym, k, paramsOpt, auxParams, parentPath, methods, - privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion) = defn + privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion, _) = defn own.foreach(_.traverse) isym.traverse sym.traverse @@ -108,6 +113,7 @@ class BlockTraverser: paramsOpt.foreach(applyParamList) auxParams.foreach(applyParamList) parentPath.foreach(applyPath) + defn.methodKeys.valuesIterator.foreach(applyPath) methods.foreach(applyFunDefn) privateFields.foreach(_.traverse) publicFields.foreach: f => @@ -123,6 +129,7 @@ class BlockTraverser: def applyCompanionModule(b: ClsLikeBody): Unit = b.isym.traverse + b.methodKeys.valuesIterator.foreach(applyPath) b.methods.foreach(applyFunDefn) b.privateFields.foreach(_.traverse) b.publicFields.foreach: f => @@ -132,9 +139,8 @@ class BlockTraverser: def applyArg(arg: Arg): Unit = applyPath(arg.value) - def applyParamList(pl: ParamList): Unit = - pl.params.foreach(_.sym.traverse) - pl.restParam.foreach(_.sym.traverse) + def applyParamList(pl: LoweredParamList): Unit = + pl.allParams.foreach(_.sym.traverse) def applyCase(cse: Case): Unit = cse match case Case.Lit(lit) => () diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/BufferableTransform.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/BufferableTransform.scala index 910564640c..8025ccf2d7 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/BufferableTransform.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/BufferableTransform.scala @@ -26,7 +26,7 @@ class BufferableTransform()(using State, Raise): val pubFieldMap: Map[Symbol, Symbol] = cls.publicFields.toMap val fields = cls.privateFields ++ cls.publicFields.map(_._2) val fieldMap: Map[Symbol, Int] = fields.zipWithIndex.toMap - def mkSymbolReplacer(params: List[ParamList]): (List[ParamList], Map[SimpleSymbol, SimpleSymbol]) = + def mkSymbolReplacer(params: List[LoweredParamList]): (List[LoweredParamList], Map[SimpleSymbol, SimpleSymbol]) = val allVars = params.flatMap(_.allParams).map(_.sym) // TODO: https://github.com/hkust-taco/mlscript/pull/504#discussion_r3827173721 val varMap = allVars @@ -35,16 +35,16 @@ class BufferableTransform()(using State, Raise): .toMap def mapParam(p: Param) = Param(p.flags, varMap(p.sym), p.sign, p.modulefulness)(p.toLoc) - (params.map(pl => ParamList(pl.flags, pl.params.map(mapParam), pl.restParam.map(mapParam))(pl.toLoc)), varMap.toMap) + (params.map(pl => LoweredParamList(pl.flags, pl.params.map(mapParam), pl.restParam.map(mapParam))(pl.toLoc)), varMap.toMap) def mkFieldReplacer(buf: VarSymbol, baseIdx: VarSymbol, symMap: Map[SimpleSymbol, SimpleSymbol]) = def getOffset(off: Int, loc: Opt[Loc])(k: Path => Block): Block = val idxSymbol = new TempSymbol(N, erasedType = S(ErasedType.Int), "idx") Scoped(Set.single(idxSymbol), Assign(idxSymbol, Call(State.builtinOpsMap("+").asSimpleRef, (baseIdx.asSimpleRef.asArg :: Value.Lit(Tree.IntLit(off))(N).asArg :: Nil) ne_:: Nil)(CallMetadata.defaultMlsFun, N), - k(DynSelect(buf.asSimpleRef.selSN("buf"), idxSymbol.asSimpleRef, true)(loc)))) + k(DynSelect(buf.asSimpleRef.selSN("buf"), idxSymbol.asSimpleRef, DynSelectKind.ArrayIdx)(loc)))) def assignToOffset(off: Int, r: Result, rst: Block) = val idxSymbol = new TempSymbol(N, erasedType = S(ErasedType.Int), "idx") Scoped(Set.single(idxSymbol), Assign(idxSymbol, Call(State.builtinOpsMap("+").asSimpleRef, (baseIdx.asSimpleRef.asArg :: Value.Lit(Tree.IntLit(off))(N).asArg :: Nil) ne_:: Nil)(CallMetadata.defaultMlsFun, N), - AssignDynField(buf.asSimpleRef.selSN("buf"), idxSymbol.asSimpleRef, true, r, applyBlock(rst)))) + AssignDynField(buf.asSimpleRef.selSN("buf"), idxSymbol.asSimpleRef, DynSelectKind.ArrayIdx, r, applyBlock(rst)))) new BlockTransformer(SymbolSubst.Id): override def applySimpleSymbol(sym: SimpleSymbol): SimpleSymbol = symMap.getOrElse(sym, sym) override def applyBlock(b: Block): Block = b match @@ -78,7 +78,7 @@ class BufferableTransform()(using State, Raise): val idx = VarSymbol(new Tree.Ident("idx"), erasedType = S(ErasedType.Int)) val (newParams, symMap) = mkSymbolReplacer(f.params) val blk = mkFieldReplacer(buf, idx, symMap).applyBlock(f.body) - FunDefn(f.owner, f.sym, TermSymbol(f.dSym.k, f.dSym.owner, f.dSym.id, erasedType = N), PlainParamList( + FunDefn(f.owner, f.sym, TermSymbol(f.dSym.k, f.dSym.owner, f.dSym.id, erasedType = N), LoweredParamList.plain( Param.simple(buf) :: Param.simple(idx) :: Nil)(N) :: newParams, if isCtor then Begin(blk, Return(idx.asSimpleRef)) else blk)(configOverride = f.configOverride, annotations = f.annotations) val fakeCtor = transformFunDefn(FunDefn.withFreshSymbol( @@ -87,13 +87,18 @@ class BufferableTransform()(using State, Raise): cls.paramsOpt.toList ::: cls.auxParams, Begin(cls.preCtor, cls.ctor), )(N, annotations = Nil), true) + val transformedMethods = cls.methods.map(transformFunDefn(_, false)) + val transformedKeys = cls.methods.zip(transformedMethods).flatMap: (before, after) => + cls.methodKeys.get(before.dSym).map(after.dSym -> _) + .toMap val fakeCompanion = ClsLikeBody( companionSym, - fakeCtor :: cls.methods.map(transformFunDefn(_, false)), + fakeCtor :: transformedMethods, Nil, clsSizeSym -> clsSizeTermSym :: Nil, Define(ValDefn(clsSizeTermSym, clsSizeSym, Value.Lit(Tree.IntLit(fields.size))(N))(N, Nil), End()), annotations = Nil, + methodKeys = transformedKeys, ) k: ClsLikeDefn( @@ -113,6 +118,7 @@ class BufferableTransform()(using State, Raise): S(fakeCompanion), cls.bufferable, cls.termCompanion, + if bufferable then cls.methodKeys else Map.empty, )(cls.configOverride, cls.annotations) case _ => super.applyDefn(defn)(k) transformer.applyProgram(prog) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/ClassParamFlattener.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/ClassParamFlattener.scala index 20bc2f7942..9fb5aec73a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/ClassParamFlattener.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/ClassParamFlattener.scala @@ -12,11 +12,13 @@ import hkmc2.semantics.Elaborator.State /** Flattens class constructor parameter lists in the Block IR. * * After this pass, every `ClsLikeDefn` is normalized so that - * `paramsOpt` is `N` and `auxParams` contains exactly one `ParamList` + * `paramsOpt` is `N` and `auxParams` contains exactly one `LoweredParamList` * (which may be empty for parameterless classes). * * Instantiations are saturated by the time this pass runs, so every * `Instantiate` can be rewritten without consulting the class definition. + * Lowering.lowerConstructorArgs has materialized omitted optional constructor + * arguments, so flattening cannot shift later lists into their parameter slots. * Source-level constructor wrapper functions keep their original calling * convention when they are used partially. Saturated calls to curried class * wrappers are lowered to instantiations, matching the `new` path without @@ -26,18 +28,18 @@ import hkmc2.semantics.Elaborator.State */ class ClassParamFlattener(using State) extends BlockTransformer(SymbolSubst.Id): - private def flattenParamLists(paramss: Ls[ParamList]): ParamList = + private def flattenParamLists(paramss: Ls[LoweredParamList]): LoweredParamList = val flags = paramss.headOption.fold(ParamListFlags.empty)(_.flags) val (init, last) = paramss.splitAt(paramss.length - 1) val params = init.flatMap(_.allParams) ::: last.flatMap(_.params) - ParamList(flags, params, last.flatMap(_.restParam).headOption)(Loc(paramss)) + LoweredParamList(flags, params, last.flatMap(_.restParam).headOption)(Loc(paramss)) /** Normalize class params so that `paramsOpt = N` and `auxParams` has exactly one element. */ private def flattenClsParams(cls: ClsLikeDefn): ClsLikeDefn = if cls.paramsOpt.isEmpty && cls.auxParams.sizeIs == 1 then return cls val paramss = cls.paramsOpt.toList ::: cls.auxParams val flatAux = paramss match - case Nil => PlainParamList(Nil)(N) + case Nil => LoweredParamList.plain(Nil)(N) case single :: Nil => single case _ => flattenParamLists(paramss) cls.copy(paramsOpt = N, auxParams = flatAux :: Nil)( diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Erasure.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Erasure.scala index 60d64761ce..10bf437948 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Erasure.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Erasure.scala @@ -170,8 +170,9 @@ final class Erasure private (using Config, Ctx, State)(using Raise): // No erased representation is read until every interpretation has been validated and every // nominal parent header has been published, including forward and external declarations. + val validatedTypes = mutable.Set.empty[TypeResolution] statements.foreach: - case term: Term => term.typeInterpretation.foreach(_.validate(Set.empty)) + case term: Term => term.typeInterpretation.foreach(_.validate(validatedTypes)) case _ => () statements.foreach: case cls: ClassLikeDef => @@ -188,19 +189,19 @@ final class Erasure private (using Config, Ctx, State)(using Raise): case _ => () case _ => () - // Public members replace inherited names; private members retain their declaration identity. + // Public members replace inherited runtime keys; private members retain their declaration identity. // A subclass cannot implement a private declaration by introducing the same name. - type MemberKey = Either[TermSymbol, Str] + type ImplementationKey = Either[TermSymbol, MemberKey] // Definitions are keyed by identity: as case classes, they would otherwise // be hashed and compared through their entire bodies on every lookup. - val effectiveMembers = mutable.Map.empty[Identity[ClassLikeDef], Map[MemberKey, TermSymbol]] + val effectiveMembers = mutable.Map.empty[Identity[ClassLikeDef], Map[ImplementationKey, TermSymbol]] def checkConstruction(term: Term & PossiblyErroneous, cls: ClassDef): Unit = if Inheritance.hasModifier(cls, syntax.Keyword.`abstract`) then term.isErroneous = true raise(ErrorReport( msg"Cannot instantiate abstract class '${cls.sym.nme}'" -> term.toLoc :: (msg"Abstract class defined here" -> cls.toLoc) :: Nil, source = Diagnostic.Source.Compilation)) - def members(cls: ClassLikeDef, visiting: Set[Identity[ClassLikeDef]]): Map[MemberKey, TermSymbol] = + def members(cls: ClassLikeDef, visiting: Set[Identity[ClassLikeDef]]): Map[ImplementationKey, TermSymbol] = effectiveMembers.getOrElseUpdate(Identity(cls), if visiting(Identity(cls)) then raise(ErrorReport(msg"Inheritance cycle involving '${cls.sym.nme}'" -> cls.toLoc :: Nil, @@ -208,10 +209,10 @@ final class Erasure private (using Config, Ctx, State)(using Raise): Map.empty else val inherited = cls.ext.flatMap(parentClass) - .fold(Map.empty[MemberKey, TermSymbol])(members(_, visiting + Identity(cls))) + .fold(Map.empty[ImplementationKey, TermSymbol])(members(_, visiting + Identity(cls))) inherited ++ cls.body.members.toList.flatMap: (name, member) => member.asTrm.map: symbol => - val key: MemberKey = if symbol.isPrivate then Left(symbol) else Right(name) + val key: ImplementationKey = if symbol.isPrivate then Left(symbol) else Right(MemberKey.symbol(member).getOrElse(name)) key -> symbol ) statements.foreach: diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala index 9ca175614c..f6ea3bc447 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/EtaExpansion.scala @@ -10,9 +10,12 @@ import utils.* import hkmc2.codegen.flowAnalysis.* import semantics.* -case class EtaTargets(paramCount: Int, hasRestParam: Bool, prodFuns: Set[ProdFun]): - def paramInfo: (Int, Bool) = paramCount -> hasRestParam - def ppParamInfo: Str = if hasRestParam then s"$paramCount+rest" else s"$paramCount" +/** `requiredParamCount` is the number of leading parameters that are not optional. */ +case class EtaTargets(paramCount: Int, requiredParamCount: Int, hasRestParam: Bool, prodFuns: Set[ProdFun]): + def paramInfo: (Int, Int, Bool) = (paramCount, requiredParamCount, hasRestParam) + def ppParamInfo: Str = + val opt = if requiredParamCount < paramCount then s"(${paramCount - requiredParamCount} optional)" else "" + if hasRestParam then s"$paramCount$opt+rest" else s"$paramCount$opt" def pp: Str = s"$ppParamInfo${prodFuns.map(_.exprId).mkString("<", ", ", ">")}" abstract class EtaExpansionResult extends FlowAnalysisSolverResult: @@ -85,10 +88,13 @@ class EtaExpansionSolver(val constraintSolver: FlowConstraintSolver, tl: TraceLo val curRes = etaExpansionTargetShapes(pf) val mergedRes = res match case N => curRes - case S(prevRes) => prevRes.zip(curRes).map: (a, b) => - assert(a.paramInfo === b.paramInfo, - s"eta expansion targets disagree on arity: ${a.pp} vs ${b.pp}") - EtaTargets(a.paramCount, a.hasRestParam, a.prodFuns ++ b.prodFuns) + case S(prevRes) => + // A returned value can choose between functions with different + // calling conventions, including required vs optional parameters. + // Expand only their common prefix: forwarding a later list needs + // one convention shared by every possible target at that level. + prevRes.zip(curRes).takeWhile((a, b) => a.paramInfo === b.paramInfo).map: (a, b) => + EtaTargets(a.paramCount, a.requiredParamCount, a.hasRestParam, a.prodFuns ++ b.prodFuns) go(S(mergedRes)) else Nil case UnknownProd => Nil @@ -108,7 +114,7 @@ class EtaExpansionSolver(val constraintSolver: FlowConstraintSolver, tl: TraceLo cache.get(pf) match case S(res) => res case N => - val targets = EtaTargets(pf.params.size, pf.restParam.isDefined, Set.single(pf)) + val targets = EtaTargets(pf.params.size, pf.requiredParamCount, pf.restParam.isDefined, Set.single(pf)) if !processing.contains(pf) then val res = targets :: funResShape(pf.res) cache(pf) = res diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/FirstClassFunctionTransformer.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/FirstClassFunctionTransformer.scala index 78183a513e..9980b87b4e 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/FirstClassFunctionTransformer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/FirstClassFunctionTransformer.scala @@ -4,7 +4,7 @@ package codegen import hkmc2.utils.*, shorthands.* import utils.* import semantics.* -import syntax.{Tree, SpreadKind} +import syntax.Tree import semantics.Elaborator.{ctx, State} import hkmc2.Message.MessageContext @@ -21,32 +21,31 @@ class FirstClassFunctionTransformer funDefns += (fun.sym -> fun) super.applyFunDefn(fun) - private def generateFCFunctionClass(p: Path, params: ParamList) = + private def generateFCFunctionClass(p: Path, params: LoweredParamList) = val clsSym = ClassSymbol( syntax.Tree.DummyTypeDef(syntax.Cls), syntax.Tree.Ident("Function$") ) val defSym = new BlockMemberSymbol("Function$", Nil, false) - val args = params.params.map(_.sym.asSimpleRef.asArg) ::: - params.restParam.toList.map(p => Arg(S(SpreadKind.Eager), p.sym.asSimpleRef)) - val callDef = FunDefn.withFreshSymbol(Some(clsSym), new BlockMemberSymbol("call", Nil, true), params :: Nil, - Return(Call(p, args ne_:: Nil)(CallMetadata.defaultMlsFun, N)))(N, annotations = Nil) + val forwarding = OptionalParams.forwarding(params, fixedArity = false) + val callDef = FunDefn.withFreshSymbol(Some(clsSym), new BlockMemberSymbol("call", Nil, true), forwarding.params :: Nil, + Return(Call(p, forwarding.args ne_:: Nil)(CallMetadata.defaultMlsFun, N)))(N, annotations = Nil) ClsLikeDefn(None, clsSym, defSym, None, syntax.Cls, None, Nil, Some(Select(State.globalThisSymbol.asThis, Tree.Ident("Function"))(Some(ctx.builtins.Function), N)(false)), callDef :: Nil, Nil, Nil, Assign.discard( Call(State.builtinOpsMap("super").asSimpleRef, Nil ne_:: Nil)(CallMetadata.defaultFun, N), - End()), End(), None, None, None)(N, annotations = Nil) + End()), End(), None, None, None, methodKeys = Map.empty)(N, annotations = Nil) - private def getParamList(l: BlockMemberSymbol): Option[ParamList] = funDefns.get(l) match + private def getParamList(l: BlockMemberSymbol): Option[LoweredParamList] = funDefns.get(l) match case Some(fd) => fd.params.headOption case _ => l.tsym.flatMap(getParamList) - private def getParamList(ts: TermSymbol): Option[ParamList] = + private def getParamList(ts: TermSymbol): Option[LoweredParamList] = ts.irFunDefn.flatMap(_.params.headOption).orElse: // FIXME: remove this fallback once imported functions have their `irFunDefn` properly linked. ts.defn.collectFirst: case TermDefinition(k = syntax.Fun, params = params @ (_ :: _)) => - params.head + LoweredParamList.callingConvention(params.head) private def isGetter(l: BlockMemberSymbol): Bool = funDefns.get(l) match case Some(fd) => fd.params.isEmpty @@ -59,7 +58,7 @@ class FirstClassFunctionTransformer case TermDefinition(k = syntax.Fun, params = Nil) => true case _ => false - private def etaExpandPath(p: Path, params: ParamList)(k: Path => Block): Block = + private def etaExpandPath(p: Path, params: LoweredParamList)(k: Path => Block): Block = val clsDef = generateFCFunctionClass(p, params) val tmp = new TempSymbol(None, erasedType = S(ErasedType.ValueLike(rsc = S(false), clsDef.isym.asClsOrMod.get))) val cls = clsDef.sym.asMemberRef(clsDef.isym) @@ -72,22 +71,31 @@ class FirstClassFunctionTransformer else etaExpandPath(ref, getParamList(l).getOrElse(lastWords(s"Cannot get ${l.nme}'s parameter list.")))(k) case _ => k(p) case sel: Select => sel.symbol match - case Some(s: TermSymbol) if (s.k is syntax.Fun) => - if isGetter(s) then k(p) - else - val params = getParamList(s).getOrElse: - raise: - ErrorReport(msg"Cannot get ${s.nme}'s parameter list." - -> sel.toLoc :: Nil, - source = Diagnostic.Source.Compilation) - PlainParamList(Nil)(N) - etaExpandPath(sel, params)(k) + case Some(s: TermSymbol) if (s.k is syntax.Fun) => etaExpandMethod(sel, s)(k) case Some(_) => k(p) case _ => raise(ErrorReport(msg"Cannot determine if ${sel.name.name} is a function." -> sel.toLoc :: Nil, source = Diagnostic.Source.Compilation)) k(p) + // * A member selected through its runtime key, such as an `@jsSymbol` method, is a method + // * reference just like a resolved `Select`. + case DynSelect(_, _, DynSelectKind.Member(s)) if s.k is syntax.Fun => etaExpandMethod(p, s)(k) case _ => k(p) + + private def etaExpandMethod(p: Path, s: TermSymbol)(k: Path => Block): Block = + if isGetter(s) then k(p) + else + val params = getParamList(s).getOrElse: + raise: + ErrorReport(msg"Cannot get ${s.nme}'s parameter list." + -> p.toLoc :: Nil, + source = Diagnostic.Source.Compilation) + LoweredParamList.plain(Nil)(N) + etaExpandPath(p, params)(k) + + /** A resolved function member is called directly; any other term member holds a function object. */ + private def resolvedCallee(fun: Path, s: TermSymbol): Path = + if s.k is syntax.Fun then fun else fun.selSN("call") override def applyResult(r: Result)(k: Result => Block): Block = r match case c @ Call(fun, argss) => applyListOf(argss, (args, k2) => applyArgs(args)(k2)): argss2 => @@ -101,13 +109,12 @@ class FirstClassFunctionTransformer case _ => k(call(fun)) case ref @ Value.MemberRef(_, _) => k(call(fun)) case sel: Select => sel.symbol match - case Some(s: TermSymbol) => - if s.k is syntax.Fun then k(call(fun)) - else k(call(sel.selSN("call"))) + case Some(s: TermSymbol) => k(call(resolvedCallee(fun, s))) case _ => raise(ErrorReport(msg"Cannot determine if ${sel.name.name} is a function object." -> fun.toLoc :: Nil, source = Diagnostic.Source.Compilation)) k(call(fun)) + case DynSelect(_, _, DynSelectKind.Member(s)) => k(call(resolvedCallee(fun, s))) case s: DynSelect => raise(ErrorReport(msg"Cannot determine if the dynamic selection is a function object." -> s.toLoc :: Nil, source = Diagnostic.Source.Compilation)) @@ -121,7 +128,7 @@ class FirstClassFunctionTransformer case Nil => fd case _ :: Nil => fd case head :: tail => - def rec(params: List[ParamList]): Block = params match + def rec(params: List[LoweredParamList]): Block = params match case head :: rest => val newBody = rec(rest) val funSym = new BlockMemberSymbol("lambda$", Nil, false) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala index 2c66104cd4..b6bcbd0606 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/FlowAnalysisBasedRewrite.scala @@ -18,7 +18,7 @@ class FlowAnalysisBasedRewrite( val deadConstructorElimSolver: DeadConstructorElimResult, )(using Raise) extends PolyInstantiationRewrite(constraintSolver): - private case class EtaParamList(params: ParamList, args: Ls[Arg]) + private case class EtaParamList(params: LoweredParamList, args: Ls[Arg]) class Rewriter(instId: InstantiationId, refreshSymbolMappingForCopy: Opt[collection.Map[LocalVarSymbol, LocalVarSymbol]]) extends InstantiationRewriter(instId, refreshSymbolMappingForCopy): @@ -32,7 +32,7 @@ class FlowAnalysisBasedRewrite( try thunk finally activeEliminatedParams --= removed - private def filterParamList(pl: ParamList, eliminable: Set[Int]): (ParamList, Set[VarSymbol]) = + private def filterParamList(pl: LoweredParamList, eliminable: Set[Int]): (LoweredParamList, Set[VarSymbol]) = if eliminable.isEmpty then pl -> Set.empty else val removed = MutSet.empty[VarSymbol] @@ -41,9 +41,9 @@ class FlowAnalysisBasedRewrite( case (param, i) => if eliminable(i) then removed.add(param.sym) else keptParams.append(param) - ParamList(pl.flags, keptParams.toList, pl.restParam)(pl.toLoc) -> removed.toSet + LoweredParamList(pl.flags, keptParams.toList, pl.restParam)(pl.toLoc) -> removed.toSet - private def filterFunParams(funSym: TermSymbol, params: Ls[ParamList]): (Ls[ParamList], Set[VarSymbol]) = + private def filterFunParams(funSym: TermSymbol, params: Ls[LoweredParamList]): (Ls[LoweredParamList], Set[VarSymbol]) = val removed = MutSet.empty[VarSymbol] var changed = false val params2 = params.zipWithIndex.map: @@ -65,11 +65,11 @@ class FlowAnalysisBasedRewrite( try thunk finally activeEtaArgss = saved - private def etaParamLists(id: ConcreteFunId, existingParams: Ls[ParamList]): Ls[EtaParamList] = + private def etaParamLists(id: ConcreteFunId, existingParams: Ls[LoweredParamList]): Ls[EtaParamList] = def eliminableParamsOf(targets: EtaTargets): Set[Int] = - val paramInfos = targets.prodFuns.map(pf => pf.params.size -> pf.restParam.isDefined) + 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 => @@ -82,19 +82,24 @@ class FlowAnalysisBasedRewrite( etaExpansionSolver.etaExpandedFunShape.get(id).toList.flatMap: targetShape => val existingShape = existingParams.map: pl => - pl.params.size -> pl.restParam.isDefined + (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 => - etaParam(i.toString) + if i < targets.requiredParamCount then etaParam(i.toString) else optionalEtaParam(i.toString) .toList val restParam = Option.when(targets.hasRestParam)(etaParam("rest")) EtaParamList( - ParamList(ParamListFlags.empty, params, restParam)(N), + 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)), ) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala index 8162618db5..4b464e2f91 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/HandlerLowering.scala @@ -61,23 +61,36 @@ object HandlerLowering: // currentFun: path to the current function for resumption // thisPath: path to `this` binding if the function is a method, `this` will be rebinded on resumption private case class FunctionCtx(currentFun: Path, thisPath: Option[Path], resumeInfo: ResumeInfo, debugInfo: DebugInfo, inGetter: Bool, inAsync: Bool): - def doUnwind(loc: Value, state: Path, restoreList: List[LocalVarSymbol])(using paths: HandlerPaths) = - Return(Call(paths.unwindPath, ( + def doUnwind(loc: Value, state: Path, restoreList: List[LocalVarSymbol])(using paths: HandlerPaths): Block = + val unwind = Return(Call(paths.unwindPath, ( currentFun :: state :: loc :: debugInfo.debugInfoPath :: thisPath.getOrElse(unit) :: - resumeInfo.argLists ++: - (intLit(restoreList.length) :: - restoreList.map(_.asPath)) + intLit(resumeInfo.argLists.length) :: + Nil + ).map(_.asArg) ::: resumeInfo.argLists.flatMap(_.encoded) ::: ( + intLit(restoreList.length) :: + restoreList.map(_.asPath) ).map(_.asArg) ne_:: Nil)(CallMetadata.mlsFunWithEffect, N)) + resumeInfo.argLists.foldRight[Block](unwind): (argList, rest) => + argList.lengthDefn.fold(rest)((sym, length) => Assign(sym, length, rest)) - // argLists: length-encoded argument list used for resumption. + /** The arguments of a parameter list, saved when unwinding so that `FunctionContFrame.resume` can pass them + * back to the function. They are encoded as their number (`length`) followed by the arguments themselves. + * A rest parameter is spread into the saved arguments, so that the resumed function collects them again. + * Omitting it would lose its arguments, including those of trailing parameters, which lowering gathers + * into a rest parameter (see `Lowering.lowerTrailingParams`). The number of arguments then depends on + * the length of the rest parameter, so `lengthDefn` computes it into a temporary before unwinding. */ + private case class SavedArgList(length: Path, args: Ls[Arg], lengthDefn: Opt[(TempSymbol, Result)]): + def encoded: Ls[Arg] = length.asArg :: args + + // argLists: the argument lists saved for resumption (see `SavedArgList`). // currentLocals: All locals to be saved and reloaded, this cannot include any variables in outer scopes // currentStackSafetySym: The symbol to be used for stack safety private case class ResumeInfo( - argLists: List[Path], + argLists: List[SavedArgList], currentLocals: List[LocalVarSymbol], currentStackSafetySym: FnOrCls, ) @@ -308,7 +321,7 @@ class HandlerLowering(paths: HandlerPaths, opt: Opt[EffectHandlers])(using TL, R case Define(defn, rest) => Define(defn, go(rest)) case Assign(lhs, rhs, rest) => Assign(lhs, rhs, go(rest)) case blk @ AssignField(lhs, nme, rhs, rest) => AssignField(lhs, nme, rhs, go(rest))(blk.symbol) - case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => AssignDynField(lhs, fld, arrayIdx, rhs, go(rest)) + case AssignDynField(lhs, fld, kind, rhs, rest) => AssignDynField(lhs, fld, kind, rhs, go(rest)) case _: Return => blk // ignored cases @@ -542,9 +555,16 @@ class HandlerLowering(paths: HandlerPaths, opt: Opt[EffectHandlers])(using TL, R List(intLit(idx), Value.Lit(Tree.StrLit(sym.nme))(N)) .map(_.asArg) val debugInfoSym = freshTmp(erasedType = S(ErasedType.Array), s"$debugNme$$debugInfo") - // TODO: properly support spread argument by calculating the correct length. - val rtArgLists = intLit(fun.params.length) :: fun.params.flatMap: pl => - intLit(pl.params.length) :: pl.params.map(p => p.sym.asSimpleRef) + val rtArgLists = fun.params.map: pl => + val args = pl.params.map(_.sym.asSimpleRef.asArg) + pl.restParam match + case N => SavedArgList(intLit(pl.params.length), args, N) + case S(rest) => + val length = freshTmp(erasedType = S(ErasedType.Int), "argCount") + val restLength = rest.sym.asSimpleRef.selSN("length") + SavedArgList(length.asSimpleRef, args :+ Arg(S(syntax.SpreadKind.Eager), rest.sym.asSimpleRef), + S(length -> Call(State.builtinOpsMap("+").asSimpleRef, + (intLit(pl.params.length).asArg :: restLength.asArg :: Nil) ne_:: Nil)(CallMetadata.defaultFun, N))) val newCtx = HandlerCtx.FunctionLike(FunctionCtx(funcPath, thisPath, ResumeInfo(rtArgLists, sortedVars, L(fun.sym)), DebugInfo(debugNme, if debugEnabled then debugInfoSym.asSimpleRef else unit), thisPath.isDefined && fun.params.isEmpty, fun.async)) val bod2 = translateBlock(fun.body, newCtx, scopedVars) @@ -566,7 +586,7 @@ class HandlerLowering(paths: HandlerPaths, opt: Opt[EffectHandlers])(using TL, R raise(lifterReport(msg"Unexpected nested function: lambdas may not function correctly." -> fun.sym.toLoc :: Nil)) val (debugInfoSym, debugInfo, fun2) = translateFunLike(fun, fun.sym.asMemberRef(fun.dSym), N, fun.sym.nme) if debugEnabled then Scoped(Set.single(debugInfoSym), Assign(debugInfoSym, Tuple(false, debugInfo)(N), k(fun2))) else k(fun2) - case defn @ ClsLikeDefn(owner, isym, sym, ctorSym, kind, paramsOpt, auxParams, parentPath, methods, privateFields, publicFields, preCtor, ctor, companion, bufferable, termCompanion) => + case defn @ ClsLikeDefn(owner, isym, sym, ctorSym, kind, paramsOpt, auxParams, parentPath, methods, privateFields, publicFields, preCtor, ctor, companion, bufferable, termCompanion, _) => if h.currentBlockIsTrulyNested && opt.isDefined then raise(lifterReport(msg"Unexpected nested class: lambdas may not function correctly." -> isym.toLoc :: Nil)) val debugInfos = mutable.ArrayBuffer.empty[(TempSymbol, List[Arg])] @@ -592,7 +612,7 @@ class HandlerLowering(paths: HandlerPaths, opt: Opt[EffectHandlers])(using TL, R if (bod.methods is newMtds) && (bod.ctor is newCtor) then bod else - ClsLikeBody(bod.isym, newMtds, bod.privateFields, bod.publicFields, newCtor, bod.annotations) + ClsLikeBody(bod.isym, newMtds, bod.privateFields, bod.publicFields, newCtor, bod.annotations, bod.methodKeys) val newPreCtor = translateCtorLike(preCtor, isym.asThis, false) val newCtor = translateCtorLike(ctor, isym.asThis, false) val c2 = @@ -701,11 +721,11 @@ class HandlerLowering(paths: HandlerPaths, opt: Opt[EffectHandlers])(using TL, R val getSavedTmp = freshTmp(erasedType = S(ErasedType.Int), "saveOffset") def getSaved(off: BigInt): (Block => Block, Path) = if off == 0 then - return (id, DynSelect(paths.runtimePath.selSN("resumeArr"), paths.runtimePath.selSN("resumeIdx"), true)(N)) + return (id, DynSelect(paths.runtimePath.selSN("resumeArr"), paths.runtimePath.selSN("resumeIdx"), DynSelectKind.ArrayIdx)(N)) val addOne = Assign(getSavedTmp, Call(State.builtinOpsMap("+").asSimpleRef, (paths.runtimePath.selSN("resumeIdx").asArg :: intLit(off).asArg :: Nil) ne_:: Nil)(CallMetadata.defaultFun, N), _) - (addOne, DynSelect(paths.runtimePath.selSN("resumeArr"), getSavedTmp.asSimpleRef, true)(N)) + (addOne, DynSelect(paths.runtimePath.selSN("resumeArr"), getSavedTmp.asSimpleRef, DynSelectKind.ArrayIdx)(N)) - val resumeArrIndexed = DynSelect(paths.runtimePath.selSN("resumeArr"), getSavedTmp.asSimpleRef, true)(N) + val resumeArrIndexed = DynSelect(paths.runtimePath.selSN("resumeArr"), getSavedTmp.asSimpleRef, DynSelectKind.ArrayIdx)(N) val plus = State.builtinOpsMap("+").asSimpleRef val preRestore = blockBuilder .assign(pcVar, paths.resumePc) @@ -733,7 +753,8 @@ class HandlerLowering(paths: HandlerPaths, opt: Opt[EffectHandlers])(using TL, R mainBody ) - val extraVars = if needsStackSafety then Set(pcVar, curDepth) else Set.single(pcVar) + val argCountVars = ctx.resumeInfo.argLists.flatMap(_.lengthDefn.map(_._1)) + val extraVars = (if needsStackSafety then Set(pcVar, curDepth) else Set.single(pcVar)) ++ argCountVars Scoped( scopedVars ++ extraVars, @@ -761,7 +782,7 @@ class HandlerLowering(paths: HandlerPaths, opt: Opt[EffectHandlers])(using TL, R val withStackSafe = needsStackSafety match case S(stackLimit) => val bodSym = BlockMemberSymbol("‹stack safe body›", Nil, false) - val bodFun = FunDefn.withFreshSymbol(N, bodSym, ParamList(ParamListFlags.empty, Nil, N)(N) :: Nil, Ret(r))(configOverride = N, annotations = Nil) + val bodFun = FunDefn.withFreshSymbol(N, bodSym, LoweredParamList(ParamListFlags.empty, Nil, N)(N) :: Nil, Ret(r))(configOverride = N, annotations = Nil) blockBuilder .scopedVars(Set.single(bodSym)) .define(bodFun) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala index be764d9dae..30a511c7f9 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lifter.scala @@ -215,7 +215,8 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): override def applyPath(p: Path): Unit = p match // A direct superclass reference, like an extends or Instantiate operand, does not // let the class escape as a value. Its receiver still needs ordinary capture analysis. - case sel @ SuperSelect(qual, RefOfDefn(S(cls: ClassSymbol), _), _) => + case sel @ SuperSelect(qual, RefOfDefn(S(cls: ClassSymbol), _), key) => + applyPath(key) applyPath(qual) cls.traverse sel.symbol.traverse @@ -246,13 +247,18 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): tsym.owner.foreach(_.traverse) sym.traverse applyPath(rhs) - case ClsLikeDefn(own, isym, sym, ctorSym, k, paramsOpt, auxParams, parentPath, methods, - privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion) + case cls @ ClsLikeDefn(own, isym, sym, ctorSym, k, paramsOpt, auxParams, parentPath, methods, + privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion, _) => own.foreach(_.traverse) isym.traverse sym.traverse termCompanion.foreach(_.traverse) + // Computed names are evaluated when this class is defined. Moving the + // class out of its enclosing invocation could read a key before its + // initialization or share the first invocation's key with later calls. + if cls.methodKeys.nonEmpty || mod.exists(_.methodKeys.nonEmpty) then ignored += isym + cls.methodKeys.valuesIterator.foreach(applyPath) // Check if `extends` is a complex expression, i.e. not just extending a class. // If it's just a class, add it to an graph where edges are class extensions. // If B extends A, then A -> B is an edge @@ -607,7 +613,7 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): S(ClassCtorSymbol(syntax.Fun, N, clsSym)), syntax.Cls, N, - PlainParamList(sortedVars.iterator.map(_.param).toList)(N) :: Nil, None, Nil, Nil, + LoweredParamList.plain(sortedVars.iterator.map(_.param).toList)(N) :: Nil, None, Nil, Nil, Nil, End(), sortedVars.iterator.foldLeft[Block](End()): @@ -615,6 +621,7 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): N, N, N, + Map.empty, )(N, Nil) (defn, sortedVars.iterator.map(x => (x.ctorSyms.local, x.valDefn.tsym)).toList) @@ -921,7 +928,7 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): // some helpers private def dupParam(p: Param): Param = p.copy(sym = VarSymbol(Tree.Ident(p.sym.nme), erasedType = p.sym.erasedType))(p.toLoc) private def dupParams(plist: List[Param]): List[Param] = plist.map(dupParam) - private def dupParamList(plist: ParamList): ParamList = + private def dupParamList(plist: LoweredParamList): LoweredParamList = plist.copy(params = dupParams(plist.params), restParam = plist.restParam.map(dupParam))(plist.toLoc) /* CONCRETE IMPLS */ @@ -1070,7 +1077,7 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): private def mkFlattenedDefn: LifterResult[FunDefn] = val newPlists = fun.params match case head :: next => head.copy(params = auxParams ::: head.params)(head.toLoc) :: next - case Nil => PlainParamList(auxParams)(N) :: Nil + case Nil => LoweredParamList.plain(auxParams)(N) :: Nil val rewriter = new BlockRewriter(N) val newBod = rewriter.rewrite(fun.body) val withCapture = addExtraSyms(newBod) @@ -1082,7 +1089,7 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): // Definition with the auxiliary parameters as a new first parameter list. private def mkAuxDefn: FunDefn = - val newPList = PlainParamList(dupParams(auxParams))(N) + val newPList = LoweredParamList.plain(dupParams(auxParams))(N) val (newPlists, syms, restSym) = fun.params match case head :: _ => val duped = dupParamList(head) @@ -1183,7 +1190,7 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): ::: capturesOrdered.map(x => capSymsMap_(x).vs) ::: passedSymsOrdered.map(x => passedSymsMap_(x).vs)) .map(Param.simple(_)) - lazy val auxParamList = PlainParamList(auxParams)(N) + lazy val auxParamList = LoweredParamList.plain(auxParams)(N) // Whether this can be lifted without the need to pass extra parameters. lazy val isTrivial = auxParams.isEmpty @@ -1197,7 +1204,7 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): def mkFlattenedDefn: FunDefn = // Symbols for the aux parameter list val auxSyms = auxParams.map(p => VarSymbol(Tree.Ident(p.sym.nme), erasedType = p.sym.erasedType)) - val auxParamListLocal = PlainParamList(auxSyms.map(Param.simple(_)))(N) + val auxParamListLocal = LoweredParamList.plain(auxSyms.map(Param.simple(_)))(N) val dupedClsAuxParams = cls.auxParams.map(dupParamList(_)) val dupedMainOpt = cls.paramsOpt.map(dupParamList(_)) @@ -1216,7 +1223,7 @@ class Lifter(topLevelBlk: Block)(using State, Raise, Config): )) // Uses the symbols from pl1. - def applyPlToPl(pl1: ParamList, pl2: ParamList): List[Arg] = (pl1.restParam, pl2.restParam) match + def applyPlToPl(pl1: LoweredParamList, pl2: LoweredParamList): List[Arg] = (pl1.restParam, pl2.restParam) match case (S(rp), S(_)) => pl1.params.foldRight(Arg(S(SpreadKind.Eager), rp.sym.asSimpleRef) :: Nil)((p, ls) => p.sym.asSimpleRef.asArg :: ls) case (N, N) => pl1.paramSyms.map(s => s.asSimpleRef.asArg) case _ => die diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/LoweredParamList.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/LoweredParamList.scala new file mode 100644 index 0000000000..4332a8f9dc --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/LoweredParamList.scala @@ -0,0 +1,50 @@ +package hkmc2 +package codegen + +import hkmc2.utils.*, shorthands.* +import hkmc2.document.* +import semantics.* +import semantics.Elaborator.State +import syntax.Tree + + +/** The parameter lists accepted by the IR: fixed parameters followed by at most one rest parameter. + * Source variadic parameters with trailing parameters must be desugared before constructing IR. + * Individual parameters retain their source metadata and erased symbols. In particular, an optional + * parameter binds `null` for a missing argument. Call rewrites must supply this default whenever + * they replace parameter binding with assignments or substitutions. */ +final case class LoweredParamList(flags: ParamListFlags, params: Ls[Param], restParam: Opt[Param]) +(val toLoc: Opt[Loc]) extends Located: + def foreach(f: Param => Unit): Unit = allParams.foreach(f) + lazy val allParams: Ls[Param] = params ::: restParam.toList + lazy val paramSyms: Ls[VarSymbol] = allParams.map(_.sym) + lazy val paramCountLB: Int = FldFlags.requiredPrefixLength(params.map(_.flags)) + def hasOptParams: Bool = paramCountLB < params.length + def paramCountUB: Bool = restParam.isEmpty + def show(using Scope, ShowCfg, Raise): Document = + flags.show + :: doc"(" :: (params.map(_.show) ::: restParam.toList.map(p => doc"...${p.show}")).mkDocument(", ") + :: doc")" + def showDbg(using DebugPrinter): Str = flags.showDbg + + (params.map(_.showDbg) ::: restParam.toList.map(p => "..." + p.showDbg)).mkString("(", ", ", ")") + +object LoweredParamList: + def plain(params: Ls[Param])(toLoc: Opt[Loc]): LoweredParamList = + LoweredParamList(ParamListFlags.empty, params, N)(toLoc) + + /** Classes and generated pattern methods have only fixed parameters. Keep the check at their + * source-to-IR boundary so a future source change cannot silently discard a variadic parameter. */ + def fixed(ps: ParamList): LoweredParamList = + assert(ps.variadic.isEmpty, "Expected fixed parameters at the source-to-IR boundary") + LoweredParamList(ps.flags, ps.params, N)(ps.toLoc) + + /** The physical argument layout shared by definitions and forwarding wrappers. + * With trailing parameters, a fresh rest parameter receives everything after the required prefix. + * This does not bind the remaining source parameters: executable definitions must also emit + * the extraction prologue in `Lowering.lowerTrailingParams`; wrappers simply forward the arguments. */ + def callingConvention(ps: ParamList)(using State): LoweredParamList = ps.variadic match + case S(Variadic(_, _ :: _)) => + val args = VarSymbol(Tree.Ident("args"), erasedType = S(ErasedType.Array)) + LoweredParamList(ps.flags, ps.params.take(ps.leadingRequiredCount), S(Param.simple(args)))(ps.toLoc) + case variadic => + LoweredParamList(ps.flags, ps.params, variadic.map(_.param))(ps.toLoc) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala index 0cd446008c..3c0acc12f4 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Lowering.scala @@ -60,7 +60,8 @@ class LoweringCtx( object LoweringCtx: def loweringCtx(using sub: LoweringCtx): LoweringCtx = sub def empty(symbolsToPreserve: Set[ScopedSymbol]) = - LoweringCtx(Map.empty, symbolsToPreserve, mayRet = false, collection.mutable.Set.empty, returnType = N, superBases = Map.empty) + LoweringCtx(Map.empty, symbolsToPreserve, mayRet = false, collection.mutable.Set.empty, returnType = N, + superBases = Map.empty) def nestFunc(returnType: Opt[ErasedType])(using sub: LoweringCtx): LoweringCtx = LoweringCtx(sub.localVals, sub.symbolsToPreserve, mayRet = true, sub.definedSymsDuringLowering, returnType, sub.superBases) def nestScoped(using sub: LoweringCtx): LoweringCtx = @@ -136,6 +137,12 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu val lowerHandlers: Bool = config.effectHandlers.isDefined + private object BuiltinRef: + def unapply(term: Term): Opt[BuiltinSymbol] = term match + case SimpleRef(sym: BuiltinSymbol) => S(sym) + case Ref(sym: BuiltinSymbol) => S(sym) + case _ => N + private lazy val wasmBinaryIntrinsicMap: Map[Str, Str] = Map( "+" -> "plus_impl", "-" -> "minus_impl", @@ -225,7 +232,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu new TailOp(transfersControl = true): override def apply(k: Result): Block = Ret(castTo(k, returnType, N)) - def parentConstructor(ctorParamLists: Ls[ParamList], args: Ls[Term], loc: Opt[Loc])(using LoweringCtx) = + def parentConstructor(ctorParamLists: ConstructorParams, args: Ls[Term], loc: Opt[Loc])(using LoweringCtx) = lowerSuperCtorCall( ctorParamLists, State.builtinOpsMap("super").asSimpleRef, @@ -412,65 +419,74 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu msg"Buffered classes must not have a main parameter list; use `constructor(...)` syntax instead." -> defn.toLoc :: Nil, source = Diagnostic.Source.Compilation )) - def lowerClass(parent: Opt[Path], parentParams: Ls[ParamList]): ClsLikeDefn = + def lowerClass(parent: Opt[Path], parentParams: ConstructorParams)(define: ClsLikeDefn => Block): Block = val base = parent.getOrElse( Select(State.globalThisSymbol.asThis, Tree.Ident("Object"))(S(ctx.builtins.Object), defn.toLoc)(false)) - // Class and module companions share one lowered definition, so class tracing may - // include its companion. Explicit module debug settings still override that scope. - debugLowering(defn.extraAnnotations)(LoweringCtx.withSuperBase(defn.sym, base).givenIn { - val (mtds, publicFlds, privateFlds, ctor) = defn match - case pd: PatternDef => - // Compile the pattern definition into `unapply` and `unapplyStringPrefix` - // methods using the `SplitCompiler`, which transliterate the pattern into - // UCS splits that backtrack without any optimizations. - val compiler = new ups.SplitCompiler - val methods = compiler.compilePattern(pd) - // We only need `owner`, `sym`, `params` and `body` - val mtds = methods.map: - case (sym, params, split) => - val paramLists = params :: Nil - val bodyBlock = inScopedBlock(ucs.Normalization(this)(split)(Ret)) - FunDefn.withFreshSymbol(N, sym, paramLists, bodyBlock)(configOverride = N, annotations = Nil) - // The return type is intended to be consistent with `gatherMembers` - (mtds, Nil, Nil, End()) - case _ => gatherMembers(defn.body) - val mod = defn.companion match - case S(sym) => - sym.defn match - case S(mod: ModuleOrObjectDef) => - reportAnnotations(mod, mod.extraAnnotations) - mod.ext match - case S(ext) => fail: - ErrorReport( - msg"Modules cannot have an extension clause." -> ext.toLoc :: Nil, - source = Diagnostic.Source.Compilation - ) - case N => - val (mtds, publicFlds, privateFlds, ctor) = - debugLowering(mod.extraAnnotations)(gatherMembers(mod.body)) - S(ClsLikeBody(mod.sym, mtds, privateFlds, publicFlds, ctor, mod.annotations)) - case _ => N - case _ => N - val pctor = defn.ext.fold[Block](End()): ext => - inScopedBlock(parentConstructor(parentParams, ext.args, ext.toLoc)) - val cfgOverride = definitionConfigOverride(defn.extraAnnotations) - ClsLikeDefn(defn.owner, defn.sym, defn.bsym, defn.ctorSym, defn.kind, defn.paramsOpt, defn.auxParams, parent, - mtds, privateFlds, publicFlds, pctor, ctor, mod, bufferable, defn.bsym.asTrm)(cfgOverride, defn.annotations) - }) + val bodies = defn.companion.flatMap(_.defn).collect: + case mod: ModuleOrObjectDef => mod.body + def companionKeys(k: Map[TermSymbol, Path] => Block): Block = bodies match + case S(body) => methodKeys(body)(k) + case N => k(Map.empty) + methodKeys(defn.body): classKeys => + companionKeys: moduleKeys => + val clsDef = debugLowering(defn.extraAnnotations)(LoweringCtx.withSuperBase(defn.sym, base).givenIn { + val (mtds, publicFlds, privateFlds, ctor) = defn match + case pd: PatternDef => + // Compile the pattern definition into `unapply` and `unapplyStringPrefix` + // methods using the `SplitCompiler`, which transliterate the pattern into + // UCS splits that backtrack without any optimizations. + val compiler = new ups.SplitCompiler + val methods = compiler.compilePattern(pd) + // We only need `owner`, `sym`, `params` and `body` + val mtds = methods.map: + case (sym, params, split) => + val paramLists = LoweredParamList.fixed(params) :: Nil + val bodyBlock = inScopedBlock(ucs.Normalization(this)(split)(Ret)) + FunDefn.withFreshSymbol(N, sym, paramLists, bodyBlock)(configOverride = N, annotations = Nil) + // The return type is intended to be consistent with `gatherMembers` + (mtds, Nil, Nil, End()) + case _ => gatherMembers(defn.body) + val mod = defn.companion match + case S(sym) => + sym.defn match + case S(mod: ModuleOrObjectDef) => + reportAnnotations(mod, mod.extraAnnotations) + mod.ext match + case S(ext) => fail: + ErrorReport( + msg"Modules cannot have an extension clause." -> ext.toLoc :: Nil, + source = Diagnostic.Source.Compilation + ) + case N => + val (mtds, publicFlds, privateFlds, ctor) = + debugLowering(mod.extraAnnotations)(gatherMembers(mod.body)) + S(ClsLikeBody(mod.sym, mtds, privateFlds, publicFlds, ctor, mod.annotations, moduleKeys)) + case _ => N + case _ => N + val pctor = defn.ext.fold[Block](End()): ext => + inScopedBlock(parentConstructor(parentParams, ext.args, ext.toLoc)) + val cfgOverride = definitionConfigOverride(defn.extraAnnotations) + ClsLikeDefn(defn.owner, defn.sym, defn.bsym, defn.ctorSym, defn.kind, + defn.paramsOpt.map(LoweredParamList.fixed), defn.auxParams.map(LoweredParamList.fixed), parent, + mtds, privateFlds, publicFlds, pctor, ctor, mod, bufferable, defn.bsym.asTrm, classKeys)(cfgOverride, defn.annotations) + }) + define(clsDef) defn.ext match - case N => Define(lowerClass(N, Nil), blockImpl(stats, res)) + case N => + debugLoweringThen[ClsLikeDefn](defn.extraAnnotations)(lowerClass(N, ConstructorParams(Nil, false))) + (Define(_, blockImpl(stats, res))) case S(ext) => assert(k isnt syntax.Mod) // modules can't extend things and can't have super calls debugLoweringThen[ClsLikeDefn](defn.extraAnnotations)(define => classOf(ext.cls, ext): (clsp, params) => - withClassParent(clsp)(parent => define(lowerClass(S(parent), params))) + withClassParent(clsp)(parent => lowerClass(S(parent), params)(define)) )(Define(_, blockImpl(stats, res))) case td: TypeDef => // * Type definitions are erased blockImpl(stats, res) blockImpl(imps ::: funs ::: rest, res)(using loweringCtx.withLocalVals(localVals)) - def classOf(trm: Term, nw: PossiblyErroneous)(k: (Path, Ls[ParamList]) => Block)(using LoweringCtx): Block = + def classOf(trm: Term, nw: PossiblyErroneous)(k: (Path, ConstructorParams) => Block)(using LoweringCtx): Block = def classValue: Block = nw match case term: Term if !nw.isErroneous => ClassValueShape.targetsOf(term) match @@ -522,20 +538,25 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case _ => classValue else subTerm(trm)(p => k(p, getClassParamLists(p))) - private def classParamLists(cls: ClassLikeDef): Ls[ParamList] = cls.paramsOpt.toList ::: cls.auxParams + /** Keep the source constructor's calling convention even when its class value is stored in a temporary. + * Foreign constructors own their argument defaults; MLscript constructors bind absent slots to null. */ + case class ConstructorParams(lists: Ls[ParamList], isMls: Bool) - def getClassParamLists(cls: Path): Ls[ParamList] = + private def classParamLists(cls: ClassLikeDef): ConstructorParams = + ConstructorParams(cls.paramsOpt.toList ::: cls.auxParams, cls.hasDeclareModifier.isEmpty) + + def getClassParamLists(cls: Path): ConstructorParams = cls.targetSymbol match case S(clsSym: ClassSymbol) => - clsSym.defn.map(classParamLists).getOrElse(Nil) - case _ => Nil + clsSym.defn.map(classParamLists).getOrElse(ConstructorParams(Nil, false)) + case _ => ConstructorParams(Nil, false) // * Lowers the `super(...)(...)` call we get from the `extends C(...)(...)` syntax - def lowerSuperCtorCall(ctorParamLists: Ls[ParamList], fr: Path, isMlsFun: Bool, args: List[Term], loc: Opt[Loc])(k: Result => Block)(using LoweringCtx): Block = + def lowerSuperCtorCall(ctorParamLists: ConstructorParams, fr: Path, isMlsFun: Bool, args: List[Term], loc: Opt[Loc])(k: Result => Block)(using LoweringCtx): Block = args match case _ :: _ => def zipArgs(remainingParamss: Ls[ParamList], args: Ls[Term], acc: Ls[Ls[Arg]]): Block = (remainingParamss, args) match - case (ps :: remainingParamss2, arg :: remainingArgs) => lowerArgs(arg, expectedParamTypes(ps)): as => + case (ps :: remainingParamss2, arg :: remainingArgs) => lowerConstructorArgs(arg, ps, ctorParamLists.isMls): as => zipArgs(remainingParamss2, remainingArgs, as ne_:: acc) case (Nil, arg :: remainingArgs) => if remainingArgs.isEmpty then @@ -550,9 +571,9 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu msg"Extending a partially applied class is not supported" -> loc :: Nil, source = Diagnostic.Source.Compilation)) k(Call(fr, acc.reverse.ne_!)(CallMetadata(isMlsFun, true, Nil), loc)) - zipArgs(ctorParamLists, args, Nil) + zipArgs(ctorParamLists.lists, args, Nil) case Nil => - if !ctorParamLists.isEmpty then + if !ctorParamLists.lists.isEmpty then raise(ErrorReport( msg"Extending a partially applied class is not supported" -> loc :: Nil, source = Diagnostic.Source.Compilation)) @@ -563,10 +584,23 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu * trying to group as many as possible into a single one * when they correspond to parameter lists of the same callee. */ def lowerMultiCall(fr: Path, isMlsFun: Bool, annotations: Ls[Annot], args: Ls[Term], loc: Opt[Loc])(k: Result => Block)(using LoweringCtx): Block = + val isConstructor = fr.targetSymbol.exists: + case _: ClassCtorSymbol => true + case _ => false + val useMlsDefaults = isMlsFun && fr.targetSymbol.match + case S(fs: TermSymbol) => fs.defn.exists(_.hasDeclareModifier.isEmpty) + case _ => false def zipArgs(remainingParamss: Ls[ParamList], remainingArgss: Ls[Term], acc: Ls[Ls[Arg]], mayRaiseEffects: Bool): Block = (remainingParamss, remainingArgss) match case (ps :: remainingParams, args :: remainingArgs) => - lowerArgs(args, expectedParamTypes(ps))(as => zipArgs(remainingParams, remainingArgs, as :: acc, mayRaiseEffects)) + 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 => + // 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 + next(OptionalParams.pad(ps.params, as).getOrElse(as)) + else next(as) case (Nil, Nil) => k(Call(fr, acc.reverse.ne_!)(CallMetadata(isMlsFun, mayRaiseEffects, annotations), loc)) case (Nil, args :: remainingArgss) => @@ -602,7 +636,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu * when they correspond to constructor parameter lists of the same class. * If fewer argument lists are provided than constructor parameter lists, eta-expands * the missing ones with fresh lambdas (avoiding reliance on mutable JS class curry semantics). */ - def lowerMultiInstantiate(mut: Bool, cls: Path, ctorParamLists: Ls[ParamList], args: Ls[Term], annotations: Ls[Annot], loc: Opt[Loc])(k: Result => Block)(using LoweringCtx): Block = + def lowerMultiInstantiate(mut: Bool, cls: Path, ctorParamLists: ConstructorParams, args: Ls[Term], annotations: Ls[Annot], loc: Opt[Loc])(k: Result => Block)(using LoweringCtx): Block = // Nullary instantiations are represented with one empty argument list, matching existing `Instantiate` usage. def buildInstantiate(argss: Ls[Ls[Arg]]): Instantiate = Instantiate(mut, cls, if argss.isEmpty then Nil :: Nil else argss)(InstantiateMetadata(annotations), loc) @@ -613,7 +647,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu def zipArgs(remainingCtorParamss: Ls[ParamList], remainingArgss: Ls[Term], acc: Ls[Ls[Arg]]): Block = (remainingCtorParamss, remainingArgss) match case (ps :: remainingParams, args :: remainingArgs) => - lowerArgs(args, expectedParamTypes(ps))(as => zipArgs(remainingParams, remainingArgs, as :: acc)) + lowerConstructorArgs(args, ps, ctorParamLists.isMls)(as => zipArgs(remainingParams, remainingArgs, as :: acc)) case (Nil, Nil) => k(buildInstantiate(acc.reverse)) case (Nil, args :: remainingArgss) => @@ -629,13 +663,18 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case Nil => buildInstantiate(accArgss) case ps :: rest => val freshSyms = ps.params.map(p => new VarSymbol(new Tree.Ident(p.sym.nme), erasedType = N)) - softTODO(ps.restParam.isEmpty, "Eta expanding rest parameters in constructor definitions is not yet supported") + softTODO(ps.variadic.isEmpty, "Eta expanding rest parameters in constructor definitions is not yet supported") val freshParams = (ps.params zip freshSyms).map((p, s) => Param(p.flags, s, N, p.modulefulness)(p.toLoc)) - val freshParamList = ParamList(ps.flags, freshParams, N)(ps.toLoc) - val freshArgs = freshSyms.map(s => Arg(N, s.asSimpleRef)) - Lambda(freshParamList, Return(etaExpand(rest, accArgss :+ freshArgs)))(Nil, loc) + val freshParamList = LoweredParamList(ps.flags, freshParams, N)(ps.toLoc) + if ctorParamLists.isMls then + val freshArgs = freshSyms.map(s => Arg(N, s.asSimpleRef)) + Lambda(freshParamList, Return(etaExpand(rest, accArgss :+ freshArgs)))(Nil, loc) + else + val forwarding = OptionalParams.forwarding(freshParamList, fixedArity = true) + Lambda(forwarding.params, + Return(etaExpand(rest, accArgss :+ forwarding.args)))(Nil, loc) k(etaExpand(remainingParamss, acc.reverse)) - if ctorParamLists.isEmpty then + if ctorParamLists.lists.isEmpty then // * Need to specially handle no-param classes args match case Nil => @@ -650,13 +689,45 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu val tmp = loweringCtx.registerTempSymbol(N, erasedType = inst.erasedValueType, "baseInst") Assign(tmp, inst, lowerRemainingCalls(tmp.asSimpleRef, remainingArgss.head, remainingArgss.tail, annotations, N)(k)) - else zipArgs(ctorParamLists, args, Nil) + else zipArgs(ctorParamLists.lists, args, Nil) + + /** Materialize omitted MLscript constructor parameters as null before optimizers + * flatten argument lists or associate fields with argument positions. + * With spreads, collect the arguments once and read the fixed parameter slots; + * bind absent optional slots to null before constructor elimination can expose them as fields, + * and keep later lists from shifting into those slots. Foreign constructors retain undefined + * in their optional slots so their native defaults can run. + */ + private def lowerConstructorArgs(arg: Term, ps: ParamList, isMls: Bool)(k: Ls[Arg] => Block)(using LoweringCtx): Block = + def capture(args: Ls[Arg]): Block = + def loop(args: Ls[Arg], params: Ls[Param], acc: Ls[Arg]): Block = (args, params) match + case (arg :: rest, param :: params) if isMls && param.flags.opt => + OptionalParams.argument(arg.value)(value => loop(rest, params, Arg(N, value) :: acc)) + case (arg :: rest, _ :: params) => loop(rest, params, arg :: acc) + case _ => k(acc.reverse ::: args) + loop(args, ps.params, Nil) + lowerArgs(arg, expectedParamTypes(ps)): 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) + else args ::: ps.params.drop(args.length).map(p => Arg(N, Value.Lit(Tree.UnitLit(false))(arg.toLoc))) + capture(padded) + else + assert(ps.variadic.isEmpty, "Constructor parameters must have a fixed layout") + val tuple = loweringCtx.registerTempSymbol(S(arg), erasedType = S(ErasedType.Array), "constructorArgs") + def bind(params: Ls[(Param, Int)], acc: Ls[Arg]): Block = params match + case (param, index) :: rest => + val slot = loweringCtx.registerTempSymbol(S(arg), erasedType = param.sym.erasedType, "constructorArg") + OptionalParams.bindFromTuple(tuple.asSimpleRef, index, slot, isMls && param.flags.opt, + bind(rest, slot.asSimpleRef.asArg :: acc)) + case Nil => k(acc.reverse) + Assign(tuple, Tuple(false, args)(arg.toLoc), bind(ps.params.zipWithIndex, Nil)) def lowerArgs(arg: Term, expectedTypes: Ls[Opt[ErasedType]])(k: Ls[Arg] => Block)(using LoweringCtx): Block = arg match case Tup(fs) => if fs.exists(e => e match - case Spd(SpreadKind.Lazy, _) => true // is lazy spread + case Spd(SpreadKind.Lazy, _, _) => true // is lazy spread case _ => false) then raise(ErrorReport( @@ -773,10 +844,43 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu source = Diagnostic.Source.Compilation) else k(prefix, memberIdent(id, N), N) + /** Computed method names are evaluated where the class is defined, so the + * annotation's ordinary resolved expression already has the right scope. + * Accesses to those methods instead use the path recovered by resolution. + */ + private def methodKeys(body: ObjBody)(k: Map[TermSymbol, Path] => Block)(using LoweringCtx): Block = + def loop(methods: Ls[TermDefinition], keys: Map[TermSymbol, Path]): Block = methods match + case Nil => k(keys) + case method :: rest => MemberKey.symbol(method) match + case S(key) => subTerm_nonTail(key.term)(path => loop(rest, keys + (method.tsym -> path))) + case N => loop(rest, keys) + loop(body.methods.filterNot(_.isErroneous), Map.empty) + + private def namedSelection(prefix: Path, name: Tree.Ident, target: Opt[DefinitionSymbol[?]], + keyAccess: Opt[Term], loc: Opt[Loc], sanitize: Bool)(k: Path => Block)(using LoweringCtx): Block = + (keyAccess, target) match + case (S(key), S(sym: TermSymbol)) => + subTerm_nonTail(key)(key => k(DynSelect(prefix, key, DynSelectKind.Member(sym))(loc))) + case (S(key), N) => subTerm_nonTail(key)(key => k(DynSelect(prefix, key, DynSelectKind.Unknown)(loc))) + case _ => + assert(target.flatMap(_.asTrm).flatMap(_.defn).flatMap(MemberKey.symbol).isEmpty, + "A symbol-keyed selection requires its resolved access path") + k(Select(prefix, name)(target, loc)(sanitize)) + + private def assignNamedField(prefix: Path, name: Tree.Ident, value: Path, rest: Block, + keyAccess: Opt[Term])(target: Opt[DefinitionSymbol[?]])(using LoweringCtx): Block = + namedSelection(prefix, name, target, keyAccess, name.toLoc, false): + case DynSelect(_, key, kind) => AssignDynField(prefix, key, kind, value, rest) + case _ => AssignField(prefix, name, value, rest)(target) + + /** A `DynSel` term (`x.[i]` or `x.(k)`) carries no resolved member, only an array-index flag. */ + private def dynSelectKind(arrayIdx: Bool): DynSelectKind = + if arrayIdx then DynSelectKind.ArrayIdx else DynSelectKind.Unknown + private def selectionPath(sel: NewSel, prefix: Path, name: Tree.Ident, - target: Opt[DefinitionSymbol[?]]): Path = sel.tupleIndex match - case S(index) => DynSelect(prefix, Value.Lit(Tree.IntLit(BigInt(index)))(N), true)(sel.toLoc) - case N => Select(prefix, name)(target, sel.toLoc)(false) + target: Opt[DefinitionSymbol[?]])(k: Path => Block)(using LoweringCtx): Block = sel.tupleIndex match + case S(index) => k(DynSelect(prefix, Value.Lit(Tree.IntLit(BigInt(index)))(N), DynSelectKind.ArrayIdx)(sel.toLoc)) + case N => namedSelection(prefix, name, target, sel.keyAccess, sel.toLoc, false)(k) private def superReference(sup: st.Super)(k: Path => Block)(using LoweringCtx): Block = if erroneousRef(sup) then compError else sup.resolvedTargets.distinct match @@ -787,7 +891,9 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu if target.k is syntax.Fun then val base = loweringCtx.superBases.getOrElse(sup.owner, lastWords("Superclass must be lowered before its members")) - k(SuperSelect(receiver, base, target.id)(target, sup.toLoc)) + sup.keyAccess match + case S(key) => subTerm_nonTail(key)(key => k(SuperSelect(receiver, base, key)(target, sup.toLoc))) + case N => k(SuperSelect(receiver, base, Value.Lit(Tree.StrLit(target.nme))(sup.toLoc))(target, sup.toLoc)) else k(Select(receiver, target.id)(S(target), sup.toLoc)(false)) case targets => fail(ErrorReport( msg"Super reference '${sup.id.name}' requires one inherited implementation" -> sup.toLoc :: @@ -870,7 +976,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu val p1 = Param(FldFlags.empty, VarSymbol(t1, erasedType = N), N, Modulefulness.none)(t1.toLoc) val p2 = Param(FldFlags.empty, VarSymbol(t2, erasedType = N), N, Modulefulness.none)(t2.toLoc) val ps = PlainParamList(p1 :: p2 :: Nil)(N) - val bod = st.App(ref, st.Tup(List(st.Ref(p1.sym)(t1, N).resolve, st.Ref(p2.sym)(t2, N).resolve)) + val bod = st.App(ref, st.Tup(List(st.SimpleRef(p1.sym)(N), st.SimpleRef(p2.sym)(N))) (Tree.Tup(Nil // FIXME should not be required (using dummy value) )))( Tree.App(Tree.Empty(), Tree.Empty()), // FIXME should not be required (using dummy value) @@ -885,7 +991,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu val t1 = new Tree.Ident("arg") val p1 = Param(FldFlags.empty, VarSymbol(t1, erasedType = N), N, Modulefulness.none)(t1.toLoc) val ps = PlainParamList(p1 :: Nil)(N) - val bod = st.App(ref, st.Tup(List(st.Ref(p1.sym)(t1, N).resolve)) + val bod = st.App(ref, st.Tup(List(st.SimpleRef(p1.sym)(N))) (Tree.Tup(Nil // FIXME should not be required (using dummy value) )))( Tree.App(Tree.Empty(), Tree.Empty()), // FIXME should not be required (using dummy value) @@ -899,11 +1005,14 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case bs: BlockMemberSymbol => disamb.flatMap(_.defn) match case S(d) if d.hasDeclareModifier.isDefined => - val sel = SynthSel(State.globalThisSymbol.ref().resolve, ref.tree)(S(bs), FlowSymbol.synthSel(ref.tree.name), N, N).withLocOf(ref).resolve + // The global property comes from the declaration; a reference may use + // an alias or have no source identifier at all. + val name = new Tree.Ident(d.sym.nme).withLocOf(ref) + val sel = SynthSel(State.globalThisSymbol.ref().resolve, name)(S(bs), FlowSymbol.synthSel(name.name), N, N).withLocOf(ref).resolve return disamb.fold(term(sel)(k))(d => term(st.Resolved(sel, d)(N))(k)) // * Note: the alternative below, which might seem more appealing, // * works but does not instrument the selection to check for `undefined`! - // return k(Value.Ref(State.globalThisSymbol).sel(ref.tree, bs).withLocOf(ref)) + // return k(Value.Ref(State.globalThisSymbol).sel(name, bs).withLocOf(ref)) case S(td: TermDefinition) if isImplicitNullaryCall(td.tsym) => // * Functions defined in local scopes with no parameter lists are getters // * and are lowered to functions with an empty parameter list @@ -977,7 +1086,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu Modulefulness.none, Annot.RaiseEffects()(N) :: Nil, N, - )(trm.toLoc, trm.toLoc) + )(trm.toLoc, trm.toLoc, N) val rewritten = st.App( State.runtimeSymbol.ref().synthSel(State.toJsAsyncSymbol), st.Tup(PlainFld(st.Blk(td :: Nil, bms.ref(ident).resolved(dsym))) :: Nil)(Tree.DummyTup) @@ -1082,14 +1191,14 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu else ref(t, annots, N, inStmtPos = inStmtPos)(k) case ref: UnresolvedRef => openSelection(ref)((prefix, member, target) => - setupNamedSelection(prefix, memberIdent(ref.id, member), target, ref.toLoc)(k)) + setupNamedSelection(prefix, memberIdent(ref.id, member), target, ref.keyAccess, ref.toLoc)(k)) case Capture(base, thru) => term(base, inStmtPos = inStmtPos)(k) case t @ st.Ref(sym) => ref(t, annots, N, inStmtPos = inStmtPos)(k) case st.Resolved(t @ st.Ref(bsym), sym) => ref(t, annots, S(sym), inStmtPos = inStmtPos)(k) - case st.App(ref @ Ref(sym: BuiltinSymbol), arg) => + case st.App(ref @ BuiltinRef(sym), arg) => arg match case st.Tup(Nil) => if !sym.nullary then raise: @@ -1156,7 +1265,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu // * operations by the dedicated case above, so they cannot be part of a curried call chain; // * otherwise, the operator would be lowered as a function value, which, for operators that are // * both unary and binary like `-`, is the binary one. - case app @ st.App(Ref(_: BuiltinSymbol), _) => (app, args) + case app @ st.App(BuiltinRef(_), _) => (app, args) case st.App(inner, innerArg) => collectAppChain(inner, innerArg :: args) case st.TyApp(inner, _) => collectAppChain(inner, args) // type args are erased case expr => (expr, args) @@ -1247,10 +1356,10 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case sel: NewSel if sel.isClassValue => classOf(sel.prefix, sel)((p, _) => conclude(p)) case sel: NewSel => newSelection(sel): (prefix, name, target) => subTerm_nonTail(prefix): p => - conclude(selectionPath(sel, p, name, target)) + selectionPath(sel, p, name, target)(conclude) case ref: UnresolvedRef => openSelection(ref): (prefix, member, target) => subTerm_nonTail(prefix): p => - conclude(Select(p, memberIdent(ref.id, member))(target, ref.toLoc)(false)) + namedSelection(p, memberIdent(ref.id, member), target, ref.keyAccess, ref.toLoc, false)(conclude) case sel @ Sel(prefix, nme) => subTerm(prefix): p => conclude(Select(p, nme)(selSymbol(sel), trm.toLoc)(false).withLocOf(sel)) @@ -1319,11 +1428,11 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case S(sym: TermSymbol) => subTerm_nonTail(prefix): p => subTerm_nonTail(rhs): r => - AssignField(p, memberIdent(ref.id, member), castTo(r, sym.erasedType, ref.toLoc), k(unit))(S(sym)) + assignNamedField(p, memberIdent(ref.id, member), castTo(r, sym.erasedType, ref.toLoc), k(unit), ref.keyAccess)(S(sym)) case N if ref.resolvedTargets.forall(_.isInstanceOf[TermSymbol]) => subTerm_nonTail(prefix): p => subTerm_nonTail(rhs): r => - AssignField(p, memberIdent(ref.id, member), r, k(unit))(N) + assignNamedField(p, memberIdent(ref.id, member), r, k(unit), ref.keyAccess)(N) case _ => fail: ErrorReport(msg"Assignment requires a term member" -> ref.toLoc :: Nil, source = Diagnostic.Source.Compilation) @@ -1332,26 +1441,27 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case N if sel.tupleIndex.nonEmpty => subTerm_nonTail(prefix): p => subTerm_nonTail(rhs): r => - AssignDynField(p, Value.Lit(Tree.IntLit(BigInt(sel.tupleIndex.get)))(trm.toLoc), true, r, k(unit)) + AssignDynField(p, Value.Lit(Tree.IntLit(BigInt(sel.tupleIndex.get)))(trm.toLoc), DynSelectKind.ArrayIdx, r, k(unit)) case S(sym: TermSymbol) => subTerm_nonTail(prefix): p => subTerm_nonTail(rhs): r => - AssignField(p, name, castTo(r, sym.erasedType, sel.toLoc), k(unit))(S(sym)) + assignNamedField(p, name, castTo(r, sym.erasedType, sel.toLoc), k(unit), sel.keyAccess)(S(sym)) case N if sel.resolvedTargets.forall(_.isInstanceOf[TermSymbol]) => subTerm_nonTail(prefix): p => subTerm_nonTail(rhs): r => - AssignField(p, name, r, k(unit))(N) + assignNamedField(p, name, r, k(unit), sel.keyAccess)(N) case _ => fail: ErrorReport(msg"Assignment requires a term member" -> sel.toLoc :: Nil, source = Diagnostic.Source.Compilation) case sel @ Sel(prefix, nme) => subTerm(prefix): p => subTerm_nonTail(rhs): r => - AssignField( + assignNamedField( p, nme, castTo(r, fieldErasedType(resolvedSelectionSymbol), sel.toLoc), k(unit), + N, )(resolvedSelectionSymbol) case sel @ SynthSel(prefix, nme) => // * See the doc in the `term` case for `SynthSel` for why we fall back to `sel.sym` here @@ -1363,20 +1473,21 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu softAssert(sym.nonEmpty, s"Missing symbol for synthetic assignment target ${sel.showDbg}") subTerm(prefix): p => subTerm_nonTail(rhs): r => - AssignField(p, memberIdent(nme, sel.sym), castTo(r, fieldErasedType(sym), sel.toLoc), k(unit))(sym) + assignNamedField(p, memberIdent(nme, sel.sym), castTo(r, fieldErasedType(sym), sel.toLoc), k(unit), N)(sym) case sel @ DynSel(prefix, fld, ai, _) => subTerm(prefix): p => subTerm_nonTail(fld): f => subTerm_nonTail(rhs): r => - AssignDynField(p, f, ai, r, k(unit)) + AssignDynField(p, f, dynSelectKind(ai), r, k(unit)) case sel @ SelProj(prefix, _, proj) => subTerm(prefix): p => subTerm_nonTail(rhs): r => - AssignField( + assignNamedField( p, memberIdent(proj, sel.sym), castTo(r, fieldErasedType(resolvedSelectionSymbol), sel.toLoc), k(unit), + N, )(resolvedSelectionSymbol) case _ => fail: ErrorReport( @@ -1408,8 +1519,8 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case sel: NewSel if sel.isClassValue => classOf(sel.prefix, sel)((p, _) => k(p)) case sup: st.Super => superReference(sup)(k) case sel: NewSel => newSelection(sel): (prefix, name, target) => - if sel.tupleIndex.nonEmpty then subTerm_nonTail(prefix)(p => k(selectionPath(sel, p, name, target))) - else setupNamedSelection(prefix, name, target, sel.toLoc)(k) + if sel.tupleIndex.nonEmpty then subTerm_nonTail(prefix)(p => selectionPath(sel, p, name, target)(k)) + else setupNamedSelection(prefix, name, target, sel.keyAccess, sel.toLoc)(k) case sel @ Sel(prefix, nme) => if sel.isErroneous then compError else @@ -1439,7 +1550,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case DynSel(prefix, fld, ai, _) => subTerm(prefix): p => subTerm_nonTail(fld): f => - k(DynSelect(p, f, ai)(trm.toLoc)) + k(DynSelect(p, f, dynSelectKind(ai))(trm.toLoc)) case DynNew(cls, args) => @@ -1458,13 +1569,14 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu val sym = isym.defn.get.bsym loweringCtx.collectScopedSym(sym) withClassParent(sr): parent => - val clsDef = LoweringCtx.withSuperBase(isym, parent).givenIn: - val (mtds, publicFlds, privateFlds, ctor) = gatherMembers(rft) - val pctor = parentConstructor(params, as, nw.toLoc) - ClsLikeDefn(N, isym, sym, N, syntax.Cls, N, Nil, S(parent), - mtds, privateFlds, publicFlds, pctor, ctor, N, N, N)(N, Nil) - val inner = new New(sym.ref().resolved(isym), Nil, N)(FlowSymbol.neww(), N) - Define(clsDef, term_nonTail(if mut then Mut(inner) else inner)(k)) + methodKeys(rft): keys => + val clsDef = LoweringCtx.withSuperBase(isym, parent).givenIn: + val (mtds, publicFlds, privateFlds, ctor) = gatherMembers(rft) + val pctor = parentConstructor(params, as, nw.toLoc) + ClsLikeDefn(N, isym, sym, N, syntax.Cls, N, Nil, S(parent), + mtds, privateFlds, publicFlds, pctor, ctor, N, N, N, keys)(N, Nil) + val inner = new New(sym.ref().resolved(isym), Nil, N)(FlowSymbol.neww(), N) + Define(clsDef, term_nonTail(if mut then Mut(inner) else inner)(k)) case Try(sub, finallyDo) => val l = loweringCtx.registerTempSymbol(S(sub), erasedType = N) @@ -1591,6 +1703,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case sym: SimpleSymbol => k(sym.asSimpleRef) case sym => lastWords(s"Unexpected symbol kind ${sym.getClass.getSimpleName}: $sym") case Ref(sym: ValueSymbol) => k(loweringCtx(sym.asPath)) + case SimpleRef(sym: LocalVarSymbol) => k(loweringCtx(sym.asSimpleRef)) case Ref(sym) => lastWords(s"Unexpected symbol kind ${sym.getClass.getSimpleName}: $sym") case SynthSel(Ref(sym: ModuleOrObjectSymbol), name) => // Module/object cross-stage references setupSymbol(sym): r1 => @@ -1651,7 +1764,7 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case Fld(_, t, _) :: rest => quote(t): r2 => val l = loweringCtx.registerTempSymbol(N, erasedType = N) Assign(l, r2, rec(rest, l.asSimpleRef :: xs)(k)) - case Spd(eager, term) :: rest => + case Spd(eager, term, _) :: rest => fail: ErrorReport( msg"Unsupported spread in quasiquote application" -> @@ -1835,19 +1948,19 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu def setupSelection(prefix: Term, nme: Tree.Ident, disamb: Opt[DefinitionSymbol[?]], loc: Opt[Loc])(k: Result => Block)(using LoweringCtx): Block = - setupNamedSelection(prefix, disamb.fold(memberIdent(nme, N))(definitionIdent(nme, _)), disamb, loc)(k) + setupNamedSelection(prefix, disamb.fold(memberIdent(nme, N))(definitionIdent(nme, _)), disamb, N, loc)(k) - private def setupNamedSelection(prefix: Term, nme: Tree.Ident, disamb: Opt[DefinitionSymbol[?]], loc: Opt[Loc]) + private def setupNamedSelection(prefix: Term, nme: Tree.Ident, disamb: Opt[DefinitionSymbol[?]], keyAccess: Opt[Term], loc: Opt[Loc]) (k: Result => Block)(using LoweringCtx): Block = subTerm(prefix): p => - k(Select(p, nme)(disamb, loc)( + namedSelection(p, nme, disamb, keyAccess, loc, !disamb.isDefined // * ^ We assume that resolved selections are well-behaved (will not yield undefined or debind a method) // || disamb.exists(_.defn.exists(_.hasDeclareModifier.isEmpty)) // * This checks `declare` members, which is normally unwanted - )) + )(k) final def setupFunctionOrByNameDef(paramLists: List[ParamList], bodyTerm: Term, name: Option[Str], returnType: Opt[ErasedType]) - (using LoweringCtx): (List[ParamList], Block) = + (using LoweringCtx): (List[LoweredParamList], Block) = val physicalParams = paramLists match case Nil => ParamList(ParamListFlags.empty, Nil, N)(N) :: Nil case ps => ps @@ -1862,9 +1975,62 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu Scoped(scopedSyms, body) def setupFunctionDef(paramLists: List[ParamList], bodyTerm: Term, name: Option[Str], returnType: Opt[ErasedType]) - (using LoweringCtx): (List[ParamList], Block) = - val scopedBody = inScopedBlock(returnedTerm(bodyTerm, returnType)) - (paramLists, scopedBody) + (using LoweringCtx): (List[LoweredParamList], Block) = + val (loweredParamLists, prologues) = paramLists.map(lowerTrailingParams(_, name)).unzip + val scopedBody = inScopedBlock: + prologues.foldRight(returnedTerm(bodyTerm, returnType))((prologue, body) => prologue(body)) + (loweredParamLists, scopedBody) + + /** Lower a parameter list whose variadic parameter has trailing parameters, as in `fun f(x, y?, ...xs, z)`, + * which JavaScript does not support. The lowered parameter list keeps the required leading parameters (`x`) + * and gathers all the other arguments into a fresh rest parameter `args`, from which the returned prologue + * extracts the other parameters, following the assignment described in `Elaborator.params`: + * the trailing parameters (`z`) take the last arguments, the optional parameters (`y`) take the remaining + * ones from left to right, and the variadic parameter (`xs`) takes the rest. + * Passes running after lowering thus only see `LoweredParamList`, which cannot contain trailing parameters. */ + protected def lowerTrailingParams(ps: ParamList, name: Opt[Str]): (LoweredParamList, LoweringCtx ?=> Block => Block) = + val loweredParams = LoweredParamList.callingConvention(ps) + ps.variadic match + case S(Variadic(variadicParam, trailing @ (_ :: _))) => + val (required, optional) = ps.params.splitAt(ps.leadingRequiredCount) + val argsSym = loweredParams.restParam.get.sym + // * The WebAssembly backend rejects variadic parameters (see `wasm.text.genFuncBody`) and lacks + // * the JavaScript runtime functions used to extract the arguments, so the body is not generated. + if config.target is CompilationTarget.Wasm then return (loweredParams, _ => compError) + val args = argsSym.asSimpleRef + def int(n: Int): Path = Value.Lit(Tree.IntLit(n))(N) + val tupleSlice = State.runtimeSymbol.asSimpleRef.sel(Tree.Ident("Tuple"), State.tupleSymbol) + .sel(Tree.Ident("slice"), State.tupleSliceSymbol) + // * `Tuple.slice(xs, i, j)` drops the first `i` and the last `j` elements of `xs`. + def slice(xs: Path, i: Int, j: Int): Result = + Call(tupleSlice, (xs :: int(i) :: int(j) :: Nil).map(_.asArg) ne_:: Nil)(CallMetadata.defaultMlsFun, N) + // * Required trailing parameters are indexed from the end after the arity check. + // * Optional slots are guarded by the leading tuple's length before they are read. + def at(xs: Path, i: Int): Path = DynSelect(xs, int(i), DynSelectKind.ArrayIdx)(N) + val k = trailing.length + // * The sanity check emitted by the code generator only knows about the lowered parameter list, + // * so we check that the arguments of the trailing parameters are present here. + val check = Option.when(config.sanityChecks.isDefined): + Call(State.runtimeSymbol.asSimpleRef.sel(Tree.Ident("checkVariadicArgs"), State.checkVariadicArgsSymbol), + (Value.Lit(Tree.StrLit(name.getOrElse("")))(N) :: int(ps.paramCountLB) :: int(required.length) :: args :: Nil) + .map(_.asArg) ne_:: Nil)(CallMetadata.defaultMlsFun, N) + val prologue: LoweringCtx ?=> Block => Block = body => + val extractions: Ls[(LocalVarSymbol, Result)] = + (variadicParam.sym -> slice(args, optional.length, k)) + :: 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) + // * 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 + val leading = loweringCtx.registerTempSymbol(N, erasedType = S(ErasedType.Array), "leadingArgs") + Assign(leading, slice(args, 0, k), optional.zipWithIndex.foldRight(extractedRest): + case ((param, index), rest) => + OptionalParams.bindFromTuple(leading.asSimpleRef, index, param.sym, true, rest)) + check.fold(extracted)(Assign.discard(_, extracted)) + (loweredParams, prologue) + case _ => (loweredParams, body => body) // TODO: reduce duplication between the two `reportAnnotations` @@ -1891,10 +2057,16 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu case Annot.Modifier(syntax.Keyword.`public` | syntax.Keyword.`private` | syntax.Keyword.`virtual`) => () case Annot.Modifier(syntax.Keyword("staged")) => () case Annot.Pure() => () + case a: Annot.Fresh => target match + case TermDefinition(k = syntax.ImmutVal) => () + case _ => warn(a, S(msg"Only immutable vals may be marked @annotations.fresh.")) case Annot.Modifier(syntax.Keyword.`open` | syntax.Keyword.`override`) => () case a: Annot.Affine => target match case TermDefinition(k = syntax.Fun) => () case _ => warn(a) + case a: Annot.JsSymbol => target match + case td: TermDefinition if (td.k is syntax.Fun) && td.flags.isMethod && !td.tsym.isPrivate => () + case _ => warn(a, S(msg"Only public methods may use @jsSymbol.")) case _: Annot.Config => () // Config annotations are handled during FunDefn creation case a: Annot.Debug => target match case _: Definition => () // Debug annotations are handled during definition elaboration and lowering @@ -1914,12 +2086,12 @@ class Lowering()(using Config, TL, Raise, State, Ctx, SymbolPrinter)(using Erasu annotations.foreach: case Annot.Untyped() => () case annot: Annot.Trm => receiver match - case st.App(Ref(_: BuiltinSymbol), _) => warn(annot) + case st.App(BuiltinRef(_), _) => warn(annot) case st.App(_, _) | New(_, _, _) | DynNew(_, _) | Mut(_: New | _: DynNew) => () case st.Resolved(_, defnSym) if isImplicitNullaryCall(defnSym) => () case _ => warn(annot) case a @ Annot.TailCall() => receiver match - case st.App(Ref(_: BuiltinSymbol), _) => warn(a, S(msg"The @tailcall annotation has no effect on calls to built-in symbols.")) + case st.App(BuiltinRef(_), _) => warn(a, S(msg"The @tailcall annotation has no effect on calls to built-in symbols.")) case st.App(_, _) => () case st.Resolved(_, defnSym) if isImplicitNullaryCall(defnSym) => () case _ => warn(a) @@ -1955,7 +2127,7 @@ trait LoweringTraceLog(instrument: Bool)(using TL, Raise, State) override def setupFunctionDef(paramLists: List[ParamList], bodyTerm: st, name: Option[Str], returnType: Opt[ErasedType]) - (using LoweringCtx): (List[ParamList], Block) = + (using LoweringCtx): (List[LoweredParamList], Block) = if instrument then // * TODO: Instrumentation collapses the trailing parameter lists into lambdas, so `fun f(a)(b): Int` is // * compiled to take `a` alone and return a function, while its erased type - which `returnType` is @@ -1967,8 +2139,10 @@ trait LoweringTraceLog(instrument: Bool)(using TL, Raise, State) softTODO(paramLists.sizeIs <= 1, s"instrumenting '${name.getOrElse("[arrow function]")}' collapses its parameter lists, " + "so its erased type no longer describes what it is compiled to") - val (ps, bod) = handleMultipleParamLists(paramLists, bodyTerm) - val instrumentedBody = setupFunctionBody(ps, bod, name, returnType) + val (unloweredPs, bod) = handleMultipleParamLists(paramLists, bodyTerm) + val (ps, prologue) = lowerTrailingParams(unloweredPs, name) + // * The trace shows the source-level parameters, which the prologue extracts before the body runs. + val instrumentedBody = inScopedBlock(prologue(setupFunctionBody(unloweredPs, bod, name, returnType))) (ps :: Nil, instrumentedBody) else super.setupFunctionDef(paramLists, bodyTerm, name, returnType) @@ -1986,18 +2160,23 @@ trait LoweringTraceLog(instrument: Bool)(using TL, Raise, State) val prevIndentLvlSym = loweringCtx.registerTempSymbol(N, erasedType = N, dbgNme = "traceLogPrevIndent") val resSym = loweringCtx.registerTempSymbol(N, erasedType = N, dbgNme = "traceLogRes") val retMsgSym = loweringCtx.registerTempSymbol(N, erasedType = N, dbgNme = "traceLogRetMsg") - val psInspectedSyms = params.params.map(p => loweringCtx.registerTempSymbol(N, erasedType = N, dbgNme = s"traceLogParam_${p.sym.nme}") -> p.sym) + // * All the parameters are shown, in their source order; the variadic parameter is shown as a spread array. + val shownParams = params.params.map("" -> _) ::: params.variadic.toList.flatMap: v => + ("..." -> v.param) :: v.trailing.map("" -> _) + val psInspectedSyms = shownParams.map: (prefix, p) => + (loweringCtx.registerTempSymbol(N, erasedType = N, dbgNme = s"traceLogParam_${p.sym.nme}"), prefix, p.sym) val resInspectedSym = loweringCtx.registerTempSymbol(N, erasedType = N, dbgNme = "traceLogResInspected") - val psSymArgs = psInspectedSyms.zipWithIndex.foldRight[Ls[Arg]](Arg(N, Value.Lit(Tree.StrLit(")"))(N)) :: Nil): - case (((s, p), i), acc) => if i == psInspectedSyms.length - 1 - then Arg(N, s.asSimpleRef) :: acc - else Arg(N, s.asSimpleRef) :: Arg(N, Value.Lit(Tree.StrLit(", "))(N)) :: acc + def str(s: Str) = Arg(N, Value.Lit(Tree.StrLit(s))(N)) + val psSymArgs = psInspectedSyms.zipWithIndex.foldRight[Ls[Arg]](str(")") :: Nil): + case (((s, prefix, p), i), acc) => + val shown = Option.when(prefix.nonEmpty)(str(prefix)).toList ::: Arg(N, s.asSimpleRef) :: Nil + if i == psInspectedSyms.length - 1 then shown ::: acc else shown ::: str(", ") :: acc val tmp1, tmp2, tmp3 = loweringCtx.registerTempSymbol(N, erasedType = N) - assignStmts(psInspectedSyms.map: (pInspectedSym, pSym) => + assignStmts(psInspectedSyms.map: (pInspectedSym, _, pSym) => pInspectedSym -> pureCall(inspectFn, Arg(N, pSym.asSimpleRef) :: Nil) *) |>: assignStmts( diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/OptionalParams.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/OptionalParams.scala new file mode 100644 index 0000000000..ca2ebd430c --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/OptionalParams.scala @@ -0,0 +1,61 @@ +package hkmc2 +package codegen + +import hkmc2.utils.*, shorthands.* +import semantics.* +import semantics.Elaborator.State +import syntax.{Tree, SpreadKind} + + +/** Optional parameters bind `null` for missing arguments. Rewrites that replace calls with + * assignments must supply that default themselves instead of relying on function entry code. */ +object OptionalParams: + case class Forwarding(params: LoweredParamList, args: Ls[Arg]) + + /** Forward optional arguments to the callee without applying MLscript defaults to foreign calls. + * Foreign constructors need fixed slots to retain their flattened argument layout; ordinary + * forwarding calls can pass the optional suffix as a spread and let the callee bind its slots. + * Note: while it doesn't make much sense to declare JS-foreign classes as curried, + * we might well want to declare Scala classes as curried, + * which on the JVM does flatten the parameter list, + * so this might be useful once we have the Java backend. */ + def forwarding(params: LoweredParamList, fixedArity: Bool)(using State): Forwarding = + assert(!fixedArity || params.restParam.isEmpty, "Constructor parameters must have a fixed layout") + if !params.hasOptParams then + Forwarding(params, params.params.map(_.sym.asSimpleRef.asArg) ::: + params.restParam.toList.map(p => Arg(S(SpreadKind.Eager), p.sym.asSimpleRef))) + else + val required = params.params.take(params.paramCountLB) + val raw = VarSymbol(Tree.Ident("args"), erasedType = S(ErasedType.Array)) + val suffix = if fixedArity then + (0 until params.params.length - required.length).map(index => + DynSelect(raw.asSimpleRef, Value.Lit(Tree.IntLit(index))(N), DynSelectKind.ArrayIdx)(params.toLoc).asArg).toList + else Arg(S(SpreadKind.Eager), raw.asSimpleRef) :: Nil + val args = required.map(_.sym.asSimpleRef.asArg) ::: suffix + val physical = LoweredParamList(params.flags, required, S(Param.simple(raw)))(params.toLoc) + Forwarding(physical, args) + + /** Complete a known argument list, declining calls that omit required parameters so callers can + * retain their arity diagnostics instead of silently supplying values for invalid calls. */ + def pad(params: Ls[Param], args: Ls[Arg]): Opt[Ls[Arg]] = + assert(args.forall(_.spread.isEmpty), "Argument count must be known before padding optional slots") + val missing = params.drop(args.length) + Option.when(missing.forall(_.flags.opt)): + args ::: missing.map(p => Value.Lit(Tree.UnitLit(true))(p.toLoc).asArg) + + /** 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 = + val value = DynSelect(args, Value.Lit(Tree.IntLit(index))(N), DynSelectKind.ArrayIdx)(sym.toLoc) + if !optional then Assign(sym, 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) + + /** 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") + 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 a9f1f0e044..a0b75f2afb 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/PolyInstantiationRewrite.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/PolyInstantiationRewrite.scala @@ -19,7 +19,7 @@ abstract class FlowAnalysisSolverResult: end FlowAnalysisSolverResult -type RewrittenFunDefn = (params: Ls[ParamList], body: Block) +type RewrittenFunDefn = (params: Ls[LoweredParamList], body: Block) abstract class PolyInstantiationRewrite(val constraintSolver: FlowConstraintSolver): @@ -110,7 +110,7 @@ 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[ParamList], body: Block, initMapping: Map[LocalVarSymbol, LocalVarSymbol]): Rewriter = + 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) params.foreach(walker.refreshParamList) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala index 2a7b3d3e6a..290d61d698 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/Printer.scala @@ -92,8 +92,8 @@ class Printer(using Config, Ctx, Raise, ShowCfg, State, SymbolPrinter): doc"set ${print(lhs)} = ${print(rhs)}; # ${print(rest)}" case asf @ AssignField(lhs, nme, rhs, rest) => doc"set ${print(lhs)}.${showMemberSymbol(nme.name, asf.symbol)} = ${print(rhs)}; # ${print(rest)}" - case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => - doc"set ${print(lhs)}${if arrayIdx then "." else "!"}${print(fld)} = ${print(rhs)}; # ${print(rest)}" + case AssignDynField(lhs, fld, kind, rhs, rest) => + doc"set ${printDynSelection(lhs, fld, kind)} = ${print(rhs)}; # ${print(rest)}" case Define(defn, rest) => doc"${printDefinition(defn)}; # ${print(rest)}" case Scoped(syms, body) => @@ -129,7 +129,7 @@ class Printer(using Config, Ctx, Raise, ShowCfg, State, SymbolPrinter): privateFields: Ls[TermSymbol], publicFields: Ls[(BlockMemberSymbol, TermSymbol)], methods: Ls[FunDefn], - auxParams: Ls[ParamList], + auxParams: Ls[LoweredParamList], preCtor: Opt[Block], ctor: Block, ctorSym: Opt[TermSymbol], @@ -163,11 +163,11 @@ class Printer(using Config, Ctx, Raise, ShowCfg, State, SymbolPrinter): then doc"" else doc" " :: braced(doc"${docPrivFlds}${docPubFlds}${docCtor}${docMethods}") - def printParamLists(paramss: Ls[ParamList])(using Scope): Document = + def printParamLists(paramss: Ls[LoweredParamList])(using Scope): Document = paramss .map: pl => val allParams = - pl.params.map(x => doc"${scope.allocateName(x.sym)}${erasedTypeAnnot(x.sym)}") ++ + pl.params.map(x => doc"${scope.allocateName(x.sym)}${if x.flags.opt then "?" else ""}${erasedTypeAnnot(x.sym)}") ++ pl.restParam.map(x => doc"...${scope.allocateName(x.sym)}${erasedTypeAnnot(x.sym)}") allParams.mkDocument("(", ", ", ")") .mkDocument("") @@ -182,7 +182,7 @@ class Printer(using Config, Ctx, Raise, ShowCfg, State, SymbolPrinter): case ValDefn(tsym, sym, rhs) => doc"val ${print(tsym)}${erasedTypeAnnot(tsym)} = ${print(rhs)}" case cls @ ClsLikeDefn(own, isym, sym, ctorSym, k, paramsOpt, auxParams, parentSym, methods, - privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion) + privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion, _) => scope.nest.givenIn: val ctorParams = printParamLists(paramsOpt.toList) val docStaged = if cls.isStaged then doc"staged " else doc"" @@ -214,14 +214,19 @@ class Printer(using Config, Ctx, Raise, ShowCfg, State, SymbolPrinter): case Value.This(sym) => doc"${print(sym)}.this" case Value.Lit(lit) => doc"${lit.idStr}" + /** Prints `q.i` for an array index, `q.[k]` for a key resolved to a member, and `q!k` otherwise. */ + private def printDynSelection(qual: Path, fld: Path, kind: DynSelectKind)(using Scope): Document = kind match + case DynSelectKind.ArrayIdx => doc"${print(qual)}.${print(fld)}" + case DynSelectKind.Member(_) => doc"${print(qual)}.[${print(fld)}]" + case DynSelectKind.Unknown => doc"${print(qual)}!${print(fld)}" + def print(path: Path)(using Scope): Document = path match - case sel @ SuperSelect(qual, base, name) => - doc"super[${print(base)}](${print(qual)}).${showMemberSymbol(name.name, S(sel.symbol))}" + case sel @ SuperSelect(qual, base, key) => + doc"super[${print(base)}](${print(qual)}).[${print(key)}]" case sel @ Select(qual, name) => val docQual = print(qual) doc"${docQual}.${showMemberSymbol(name.name, sel.symbol)}" - case DynSelect(qual, fld, arrayIdx) => - doc"${print(qual)}${if arrayIdx then "." else "!"}${print(fld)}" + case DynSelect(qual, fld, kind) => printDynSelection(qual, fld, kind) case x: Value => print(x) case Cast(value, target, check) => // * `as!` and `as!!` mark a cast asserted with and without a runtime check respectively. @@ -239,10 +244,7 @@ class Printer(using Config, Ctx, Raise, ShowCfg, State, SymbolPrinter): doc"${printAnnotations(inst.metadata.annotations, doc" ")}new ${if mut then "mut " else ""}${print(cls)}${chainedArgs}" case Lambda(params, body) => scope.nest.givenIn: - val allParams = - params.params.map(x => doc"${scope.allocateName(x.sym)}${erasedTypeAnnot(x.sym)}") ++ - params.restParam.map(x => doc"...${scope.allocateName(x.sym)}${erasedTypeAnnot(x.sym)}") - val docParams = allParams.mkDocument("(", ", ", ")") + val docParams = printParamLists(params :: Nil) doc"$docParams => ${bracedbk(print(body))}" case Tuple(mut, elems) => val docElems = elems.map(x => print(x)).mkDocument(", ") diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/ReflectionInstrumenter.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/ReflectionInstrumenter.scala index 984dc75eee..06a37f23d1 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/ReflectionInstrumenter.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/ReflectionInstrumenter.scala @@ -102,7 +102,8 @@ class ReflectionInstrumenter(using State, Raise, Ctx) extends BlockTransformer(n case baseSym: BaseTypeSymbol => val name = scope.allocateOrGetName(baseSym) val (owner, bsym, paramsOpt, auxParams) = (baseSym.defn, defnMap.get(baseSym)) match - case (S(defn), _) => (defn.owner, defn.bsym, defn.paramsOpt, defn.auxParams) + case (S(defn), _) => (defn.owner, defn.bsym, + defn.paramsOpt.map(LoweredParamList.fixed), defn.auxParams.map(LoweredParamList.fixed)) case (_, S(defn: ClsLikeDefn)) => (defn.owner, defn.sym, defn.paramsOpt, defn.auxParams) // FIXME: hack to patch in staging for returning the object Unit. case _ if baseSym == State.unitSymbol => (N, baseSym, N, Nil) @@ -183,10 +184,11 @@ class ReflectionInstrumenter(using State, Raise, Ctx) extends BlockTransformer(n .getOrElse(blockCtor("Symbol", Ls(toValue(name)))) sym: sym => blockCtor("Select", Ls(x, sym), "sel")(k) - case DynSelect(qual, fld, arrayIdx) => + case DynSelect(qual, fld, kind) => + // * The reflected `Block.DynSelect` only distinguishes array indices; it has no member symbols. transformPath(qual): x => transformPath(fld): y => - blockCtor("DynSelect", Ls(x, y, toValue(arrayIdx)), "dynsel")(k) + blockCtor("DynSelect", Ls(x, y, toValue(kind.isArrayIdx)), "dynsel")(k) case Cast(value, target, check) => transformResult(value): v => blockCtor("Symbol", Ls(toValue(target.describe)), "target"): t => @@ -261,10 +263,10 @@ class ReflectionInstrumenter(using State, Raise, Ctx) extends BlockTransformer(n def transformArgs(args: Ls[Arg])(using Context)(k: Ls[(Path, Bool)] => Block): Block = args.map(transformArg).collectApply(k) - def transformParamList(ps: ParamList)(k: Path => Block) = + def transformParamList(ps: LoweredParamList)(k: Path => Block) = ps.params.map(p => transformSymbol(p.sym)).collectApply(tuple(_)(k)) - def transformParamsOpt(pOpt: Opt[ParamList])(k: Path => Block) = + def transformParamsOpt(pOpt: Opt[LoweredParamList])(k: Path => Block) = transformOption(pOpt, transformParamList)(k) def transformCase(cse: Case)(using Context)(k: Path => Block): Block = cse match @@ -387,7 +389,7 @@ class ReflectionInstrumenter(using State, Raise, Ctx) extends BlockTransformer(n // TODO: remove it. only for test val debug = (k: Block) => call(sym, Nil)(fnPrintCode(_)(k)) - val newFun = f.copy(sym = genSym, dSym = dSym, params = Ls(PlainParamList(Nil)(N)), body = newBody)(f.configOverride, f.annotations) + val newFun = f.copy(sym = genSym, dSym = dSym, params = Ls(LoweredParamList.plain(Nil)(N)), body = newBody)(f.configOverride, f.annotations) (newFun, debug) override def applyBlock(b: Block): Block = super.applyBlock(b) match @@ -398,7 +400,7 @@ class ReflectionInstrumenter(using State, Raise, Ctx) extends BlockTransformer(n val (stagedMethods, debugPrintCode) = companion.methods .map(applyFunDefnInner) .unzip - val ctor = FunDefn.withFreshSymbol(S(companion.isym), BlockMemberSymbol("ctor$", Nil), Ls(PlainParamList(Nil)(N)), companion.ctor)(N, Nil) + val ctor = FunDefn.withFreshSymbol(S(companion.isym), BlockMemberSymbol("ctor$", Nil), Ls(LoweredParamList.plain(Nil)(N)), companion.ctor)(N, Nil) val (stagedCtor, ctorPrint) = applyFunDefnInner(ctor) val debugBlock = (ctorPrint :: debugPrintCode) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/ScopeData.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/ScopeData.scala index d3d3d1f980..5911635c67 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/ScopeData.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/ScopeData.scala @@ -118,7 +118,7 @@ object ScopeData: case Companion(clsBody, compDefn) => Set(clsBody.isym) case _: ClassCtor => Set.empty case Func(fun, _) => fun.params.flatMap: p => - p.restParam.map(_.sym) ++ p.params.map(_.sym) + p.paramSyms .toSet case ScopedBlock(_, block) => block.syms.toSet case _: Loop => Set.empty @@ -185,8 +185,8 @@ object ScopeData: traverser.applySymbol(s) obj match case ScopedObject.Top(b) => traverser.applyBlock(b) - case ScopedObject.Class(ClsLikeDefn(own, isym, sym, ctorSym, k, paramsOpt, auxParams, parentPath, methods, - privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion), _) + case ScopedObject.Class(cls @ ClsLikeDefn(own, isym, sym, ctorSym, k, paramsOpt, auxParams, parentPath, methods, + privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion, _), _) => // do not traverse the companion -- it is a separate kind of scoped object // and will therefore be traversed separately @@ -198,6 +198,7 @@ object ScopeData: paramsOpt.foreach(traverser.applyParamList) auxParams.foreach(traverser.applyParamList) parentPath.foreach(traverser.applyPath) + cls.methodKeys.valuesIterator.foreach(traverser.applyPath) methods.foreach(traverser.applyFunDefn) privateFields.foreach(_.traverse) publicFields.foreach: f => diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/SpecializedSwitch.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/SpecializedSwitch.scala index 98d3bc404f..eb3f15179a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/SpecializedSwitch.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/SpecializedSwitch.scala @@ -153,7 +153,7 @@ private[codegen] final class PostCondAnalysis extends CachedAnalysis[Block, Post case Assign(NoSymbol, rhs, rest) => res(N, rhs, rest) case Assign(lhs: ValueSymbol, rhs, rest) => res(S(lhs), rhs, rest) case AssignField(path, _, rhs, rest) => res(N, rhs, rest) - case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => res(N, rhs, rest) + case AssignDynField(lhs, fld, kind, rhs, rest) => res(N, rhs, rest) case Define(defn, rest) => defn match case v: ValDefn => res(S(v.sym), v.rhs, rest) case c: ClsLikeDefn => analyze(rest).markImpure // TODO: refine for object and module ctors diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/SymbolRefresher.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/SymbolRefresher.scala index 23022d7fa3..8476e5c206 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/SymbolRefresher.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/SymbolRefresher.scala @@ -59,7 +59,7 @@ class SymbolRefresherWalker(mapping: MutMap[Symbol, Symbol])(using State) extend s.owner.map(o => mapping.getOrElse(o, o).asInstanceOf[InnerSymbol]), mapping.getOrElse(s.associatedCls, s.associatedCls).asInstanceOf[ClassSymbol])) - def refreshParamList(pl: ParamList) = + def refreshParamList(pl: LoweredParamList) = for p <- pl.restParam ++: pl.params do @@ -96,7 +96,7 @@ class SymbolRefresherWalker(mapping: MutMap[Symbol, Symbol])(using State) extend val ClsLikeDefn( owner, isym, sym, ctorSym, k, paramsOpt, auxParams, parentPath, methods, privateFields, publicFields, preCtor, ctor, companion, - bufferable, termCompanion) = defn + bufferable, termCompanion, _) = defn assert(mapping.isDefinedAt(sym), s"BlockMemberSymbol ${sym} for ClsLikeDefn is a free variable for this block") isym match case s: ClassSymbol => refreshClassSymbol(s) @@ -121,7 +121,7 @@ class SymbolRefresherWalker(mapping: MutMap[Symbol, Symbol])(using State) extend def applyClsLikeBody(b: ClsLikeBody): Unit = val ClsLikeBody( - isym, methods, privateFields, publicFields, ctor, annotations + isym, methods, privateFields, publicFields, ctor, annotations, _ ) = b isym match case s: ModuleOrObjectSymbol => refreshModuleOrObjectSymbol(s) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala index 9d08855a60..cf4a1a4366 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/TailRecOpt.scala @@ -236,7 +236,7 @@ class TailRecOpt(checkAnnotations: Bool)(using Config, State, TL, Raise, Ctx): // Tries to rewrite an application of an arg list to a param list without needing to forcibly // unspread the arguments. - private def rewriteArgsList(f: FunDefn, paramList: ParamList, args: List[Arg]): CallArgsResult = + private def rewriteArgsList(f: FunDefn, paramList: LoweredParamList, args: List[Arg]): CallArgsResult = val (headArgs, restArgs) = paramList.restParam match case Some(value) => args.splitAt(paramList.params.length) case None => (args, Nil) @@ -251,14 +251,24 @@ class TailRecOpt(checkAnnotations: Bool)(using Config, State, TL, Raise, Ctx): case _ => a.value if hasSpread then return CallArgsResult.ForceSpread + // * Arguments in excess of the parameters are ignored at runtime (as in JavaScript); + // * the general path evaluates them and only extracts the arguments matching the parameters. + if hd.length > paramList.params.length then return CallArgsResult.ForceSpread + + // * Optional parameters whose arguments are omitted must be reset to `null`, + // * rather than keeping the values they had in the previous iteration. + val hdPadded = OptionalParams.pad(paramList.params, headArgs) match + case S(args) => args.map(_.value) + case N => return CallArgsResult.ForceSpread + if paramList.restParam.isDefined then val rest = restArgs match case Arg(S(SpreadKind.Eager), value) :: Nil => value case _ => Tuple(true, restArgs)(N) - CallArgsResult.Success(hd.appended(rest)) + CallArgsResult.Success(hdPadded.appended(rest)) else - CallArgsResult.Success(hd) + CallArgsResult.Success(hdPadded) /** * Applies a parameter list to an argument list, both of which could contain arbitrary spreads. @@ -271,7 +281,7 @@ class TailRecOpt(checkAnnotations: Bool)(using Config, State, TL, Raise, Ctx): * - The second return value is a list of paths containing the arguments in order. If there is a spread parameter, it will be * the last one. */ - def forceSpread(plist: ParamList, args: List[Arg]): (Block => Block, List[Path]) = + def forceSpread(plist: LoweredParamList, args: List[Arg]): (Block => Block, List[Path]) = // Forcibly spread the args in an array. // Assume the lengths are correct val paramList = plist.params @@ -281,9 +291,16 @@ class TailRecOpt(checkAnnotations: Bool)(using Config, State, TL, Raise, Ctx): val tupleRes = Tuple(false, args)(N) - // Main args - def mainArgs(rest: List[Path]) = (0 until paramList.size).toList.foldRight(rest): - case (n, acc) => DynSelect(tupleSym.asSimpleRef, Value.Lit(Tree.IntLit(n))(N), true)(N) :: acc + var prefix = blockBuilder.assignScoped(tupleSym, tupleRes) + // Materialize optional slots before assigning any loop parameter, so missing arguments reset + // the slots to null and reads remain independent of parameter assignment order. + val mainArgs = paramList.zipWithIndex.map: (param, index) => + if param.flags.opt then + val slot = TempSymbol(N, erasedType = param.sym.erasedType, "optionalArg") + prefix = prefix.scopedVars(Set(slot)).chain(rest => + OptionalParams.bindFromTuple(tupleSym.asSimpleRef, index, slot, true, rest)) + slot.asSimpleRef + else DynSelect(tupleSym.asSimpleRef, Value.Lit(Tree.IntLit(index))(N), DynSelectKind.ArrayIdx)(N) // If the rest param exists, append a slice val (initialBlk: (Block => Block), pathList: List[Path]) = @@ -299,12 +316,10 @@ class TailRecOpt(checkAnnotations: Bool)(using Config, State, TL, Raise, Ctx): :: Value.Lit(Tree.IntLit(0))(N).asArg :: Nil) ne_:: Nil )(CallMetadata.defaultMlsFun, N) - val blk = blockBuilder - .assignScoped(tupleSym, tupleRes) - .assignScoped(sliceResSym, sliceRes) - (blk, mainArgs(sliceResSym.asSimpleRef :: Nil)) + val blk = prefix.assignScoped(sliceResSym, sliceRes) + (blk, mainArgs :+ sliceResSym.asSimpleRef) else - (blockBuilder.assignScoped(tupleSym, tupleRes), mainArgs(Nil)) + (prefix, mainArgs) end val (initialBlk, pathList) @@ -764,9 +779,9 @@ class TailRecOpt(checkAnnotations: Bool)(using Config, State, TL, Raise, Ctx): val newParamLists = val initial = paramSyms.map(Param.simple(_)) if funsLen > 1 then - PlainParamList(Param.simple(curIdSym) :: initial)(N) :: Nil + LoweredParamList.plain(Param.simple(curIdSym) :: initial)(N) :: Nil else - PlainParamList(initial)(N) :: Nil + LoweredParamList.plain(initial)(N) :: Nil val annotations = if funsLen == 1 && funs.head.inline then Annot.Inline()(N) :: Annot.Private :: Nil else Annot.Private :: Nil diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/WorkerWrapper.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/WorkerWrapper.scala index 256a14715d..4165855af1 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/WorkerWrapper.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/WorkerWrapper.scala @@ -30,8 +30,11 @@ class WorkerWrapper private def withoutInline(annotations: Ls[Annot]): Ls[Annot] = annotations.filterNot(_.isInstanceOf[Annot.Inline]) - private def isPlainParamList(params: ParamList): Bool = - params.flags == ParamListFlags.empty && params.restParam.isEmpty + // * Optional parameters are excluded because uncurrying concatenates the parameter lists, + // * so the arguments of a later list would be assigned to the omitted optional parameters. + // * TODO: support this once we normalize known-call sites in the future. + private def isPlainParamList(params: LoweredParamList): Bool = + params.flags == ParamListFlags.empty && params.restParam.isEmpty && !params.hasOptParams private def effectiveConfig(fun: FunDefn): Config = fun.configOverride.getOrElse(config) @@ -49,11 +52,11 @@ class WorkerWrapper mapping(param.sym) = freshSym Param(param.flags, freshSym, param.sign, param.modulefulness)(param.toLoc).withSignTypeOf(param) - private def flattenParams(params: Ls[ParamList]): (ParamList, Map[Symbol, Symbol]) = + private def flattenParams(params: Ls[LoweredParamList]): (LoweredParamList, Map[Symbol, Symbol]) = val mapping = collection.mutable.LinkedHashMap.empty[Symbol, Symbol] val flatParams = params.flatMap: params => params.params.map(freshParam(_, mapping)) - PlainParamList(flatParams)(Loc(params)) -> mapping.toMap + LoweredParamList.plain(flatParams)(Loc(params)) -> mapping.toMap private def rewriteParams(body: Block, mapping: Map[Symbol, Symbol]): Block = val subst = new SymbolSubst: 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 ca3fe73105..d1e9166754 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Rewrite.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/deforest/Rewrite.scala @@ -31,8 +31,8 @@ class DeforestRewriter(val solver: DeforestFusionSolver, additionalTopLevelSymbo matchOrLabelId match case l: LabelSymbol => l -> instId case scrutId: ResultId => ConcreteId(scrutId, instId) - extension (vs: Ls[VarSymbol]) def asParamList: ParamList = - ParamList(ParamListFlags.empty, vs.map(Param.simple), N)(N) + extension (vs: Ls[VarSymbol]) def asParamList: LoweredParamList = + LoweredParamList(ParamListFlags.empty, vs.map(Param.simple), N)(N) extension (c: CtorCls) def ctorClsName: String = c match case cls: ClassLikeSymbol => cls.nme case n: Int => s"tup$n" 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 5123393983..06ea839f99 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/flowAnalysis/FlowAnalysis.scala @@ -155,12 +155,11 @@ object TrackableSelect: object MemberRefTo: def unapply(p: Path): Opt[(targetSymbol: DefinitionSymbol[?], selectedFrom: Opt[Path])] = p.targetSymbol.map: target => - target -> - locally: - p match - case Select(qual, _) => S(qual) - case SuperSelect(qual, _, _) => S(qual) - case _ => N + target -> p.match + case Select(qual, _) => S(qual) + case DynSelect(qual, _, DynSelectKind.Member(_)) => S(qual) + case SuperSelect(qual, _, _) => S(qual) + case _ => N object CtorProducer: /** Extracts result forms that produce a concrete `Ctor` flow strategy. */ @@ -236,10 +235,13 @@ class ProdFun( val instantiationId: Opt[InstantiationId] )( val params: Ls[ConsStrat], + /** The number of leading `params` that are not optional (see `LoweredParamList.paramCountLB`). */ + val requiredParamCount: Int, val restParam: Opt[ConsStrat], val res: ProdStrat, val capturedVarUpperbound: StratVar ) extends ProdStrat with StratWithOrigin[FunId]: + def paramInfo: (Int, Int, Bool) = (params.size, requiredParamCount, restParam.isDefined) val dests = MutSet.empty[ConsFun | MarkerConsStrat] override def toString(): String = s"(${params.map(_.toString()).mkString(", ")}) -> ${res.toString()}" @@ -401,7 +403,7 @@ class FlowPreAnalyzer(val pgrm: Program)(using inline def inFun(fun: FunDefn)(inline body: => Any) = // each param list is a closure over the params of the lists before it - def withParamListCaptureInfo(paramLists: Iterator[(ParamList, Int)]): Unit = paramLists.nextOption() match + def withParamListCaptureInfo(paramLists: Iterator[(LoweredParamList, Int)]): Unit = paramLists.nextOption() match case N => body case S((pl, whichParamList)) => val locallyDefined = MutSet.from[Symbol](pl.paramSyms) @@ -440,8 +442,7 @@ class FlowPreAnalyzer(val pgrm: Program)(using inline def inLam(lam: Lambda)(inline body: => Any) = val locallyDefined = MutSet.empty[Symbol] - lam.params.params.foreach(p => locallyDefined += p.sym) - lam.params.restParam.foreach(p => locallyDefined += p.sym) + lam.params.paramSyms.foreach(locallyDefined += _) withCtx(InCtx.Lam(lam))(withCaptureInfo(lam.uid, locallyDefined)(body))() inline def inTopLvl(inline body: => Any) = @@ -551,7 +552,7 @@ class FlowPreAnalyzer(val pgrm: Program)(using applyPath(lhs) applyResult(rhs) applyBlock(rest) - case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => + case AssignDynField(lhs, fld, kind, rhs, rest) => applyPath(lhs) applyPath(fld) applyResult(rhs) @@ -595,8 +596,6 @@ class FlowPreAnalyzer(val pgrm: Program)(using case _ => () override def applyPath(p: Path): Unit = p match - case DynSelect(qual, fld, arrayIdx) => - applyPath(qual); applyPath(fld) case p@TrackableFieldSelect(qual, _ -> _) => res.selToCtxOfSel.addOne(p.uid -> ctxTracker.getAllCtx) qual match @@ -607,7 +606,7 @@ class FlowPreAnalyzer(val pgrm: Program)(using if ctxTracker.isEnclosingMatchScrutSym(disamb) => applyValueMemberRef(v, recordAffinity = false) case _ => applyPath(qual) - case p: (Select | SuperSelect) => + case p: (Select | SuperSelect | DynSelect) => super.applyPath(p) case c: Cast => applyResult(c.value) case v: Value => applyValue(v) @@ -637,9 +636,8 @@ class FlowPreAnalyzer(val pgrm: Program)(using currentCaptureInfo.foreach(_.locallyDefined += defn.tsym) applyPath(defn.rhs) - override def applyParamList(pl: ParamList): Unit = - pl.params.foreach(p => ctxTracker.registerStratVar(p.sym, p.sym.nme)) - pl.restParam.foreach(p => ctxTracker.registerStratVar(p.sym, p.sym.nme)) + override def applyParamList(pl: LoweredParamList): Unit = + pl.allParams.foreach(p => ctxTracker.registerStratVar(p.sym, p.sym.nme)) override def applyArg(arg: Arg): Unit = applyPath(arg.value) @@ -648,8 +646,10 @@ class FlowPreAnalyzer(val pgrm: Program)(using case defn: FunDefn => applyFunDefn(defn) case defn: ValDefn => applyValDefn(defn) case cls@ClsLikeDefn(own, isym, sym, ctorSym, k, paramsOpt, auxParams, parentPath, methods, - privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion) + privateFields, publicFields, preCtor, ctor, mod, bufferable, termCompanion, _) => + cls.methodKeys.valuesIterator.foreach(applyPath) + mod.foreach(_.methodKeys.valuesIterator.foreach(applyPath)) ctxTracker.inCls(cls): paramsOpt.foreach(applyParamList) auxParams.foreach(applyParamList) @@ -827,6 +827,7 @@ class FlowConstraintsCollector( case p: ProdFun => new ProdFun(p.exprId, updateInstantiationId(p.instantiationId))( p.params.map(duplicateConsStrat), + p.requiredParamCount, p.restParam.map(duplicateConsStrat), duplicateProdStrat(p.res), duplicateVarState(p.capturedVarUpperbound)) @@ -864,7 +865,7 @@ class FlowConstraintsCollector( def mkFunProdStrat( resName: String, - params: Ls[ParamList], + params: Ls[LoweredParamList], body: Block, funLamId: FunId )(using cc: ConstraintsCollector): ProdStrat = @@ -876,9 +877,13 @@ class FlowConstraintsCollector( lambdaExprId case other => lastWords(s"unexpected funLamId shape: $other") val res = freshVar(resName, cc.forFun) - params.foreach: - _.restParam.foreach: p => + params.foreach: ps => + ps.restParam.foreach: p => generatedVars(p.sym).constrainOpaque + // * The argument of an optional parameter may be omitted, in which case the parameter is + // * `null`, a value that is not tracked by the analysis. + ps.params.drop(ps.paramCountLB).foreach: p => + cc.constrain(UnknownProd, generatedVars(p.sym)) val funValueStrat = params.zipWithIndex.foldRight[ProdStrat](res): case ((ps, whichParamList), acc) => val plFunId = paramListFunId(whichParamList) @@ -890,6 +895,7 @@ class FlowConstraintsCollector( cc.constrain(capturedSymStrat, capUB) new ProdFun(plFunId, cc.instId)( ps.params.map(p => generatedVars(p.sym)), + ps.paramCountLB, ps.restParam.map(p => generatedVars(p.sym)), acc, capUB) @@ -908,6 +914,7 @@ class FlowConstraintsCollector( cc.constrain(funProdStrat, generatedVars(fun.dSym)) def processClsLikeDefn(cls: ClsLikeDefn)(using cc: ConstraintsCollector): Unit = + cls.methodKeys.valuesIterator.foreach(constrainOpaqueResult) cls.privateFields.foreach(sym => generatedVars(sym).constrainOpaque) cls.publicFields.foreach: (_, tsym) => generatedVars(tsym).constrainOpaque @@ -918,6 +925,7 @@ class FlowConstraintsCollector( processBlock(cls.preCtor)(using cc, UnknownCons) processBlock(cls.ctor)(using cc, UnknownCons) cls.companion.foreach: mod => + mod.methodKeys.valuesIterator.foreach(constrainOpaqueResult) mod.privateFields.foreach(sym => generatedVars(sym).constrainOpaque) mod.publicFields.foreach: (_, tsym) => generatedVars(tsym).constrainOpaque @@ -963,7 +971,7 @@ class FlowConstraintsCollector( constrainOpaqueResult(lhs) constrainOpaqueResult(rhs) processBlock(rest) - case AssignDynField(lhs, fld, arrayIdx, rhs, rest) => + case AssignDynField(lhs, fld, kind, rhs, rest) => constrainOpaqueResult(lhs) constrainOpaqueResult(fld) constrainOpaqueResult(rhs) @@ -1049,13 +1057,20 @@ class FlowConstraintsCollector( case p: Path => p match case refSite@FunRef(f, selectedFrom) => + refSite match + case DynSelect(_, key, DynSelectKind.Member(_)) => cc.constrain(processResult(key), UnknownCons) + case SuperSelect(_, base, key) => + cc.constrain(processResult(base), UnknownCons) + cc.constrain(processResult(key), UnknownCons) + case _ => () for qual <- selectedFrom do cc.constrain(processResult(qual), UnknownCons) funsToProdStratScheme.get(f) match case Some(fScheme) => fScheme.instantiate(refSite.uid, f) case None => generatedVars(f) - case s @ SuperSelect(qual, base, _) => + case s @ SuperSelect(qual, base, key) => + cc.constrain(processResult(key), UnknownCons) cc.constrain(processResult(qual), UnknownCons) cc.constrain(processResult(base), UnknownCons) generatedVars(s.symbol) @@ -1063,10 +1078,12 @@ class FlowConstraintsCollector( cc.constrain(processResult(qual), UnknownCons) s.symbol.fold(UnknownProd): selSym => generatedVars(selSym) - case DynSelect(qual, fld, arrayIdx) => + case DynSelect(qual, fld, kind) => cc.constrain(processResult(qual), UnknownCons) cc.constrain(processResult(fld), UnknownCons) - UnknownProd + kind match + case DynSelectKind.Member(sym) => generatedVars(sym) + case DynSelectKind.ArrayIdx | DynSelectKind.Unknown => UnknownProd case Cast(value, _, _) => val valueStrat = processResult(value) cc.constrain(valueStrat, UnknownCons) diff --git a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala index 8ef8616207..5b20024ddb 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/codegen/js/JSBuilder.scala @@ -70,7 +70,10 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: source = Diagnostic.Source.Compilation)) doc" # ${mkErr(errMsg)};" - private def getPrivateAccessorSymbol(ts: DefinitionSymbol[?]): semantics.TempSymbol = + private def getPrivateAccessorSymbol(ts: DefinitionSymbol[?])(using Raise): semantics.TempSymbol = + // Accessor symbols are shared only within this emitted module. Allocating a + // fresh symbol in an importer cannot reach the defining module's accessor. + softAssert(ts.getState is State, s"Cannot generate a cross-file access to private member '${ts.nme}'") privateAccessorSymbols.getOrElseUpdate(ts, semantics.TempSymbol(N, erasedType = N, s"${ts.nme}$$accessorSymbol")) /** Private names belong to the containing IR class, shared by its instance and static members. @@ -128,6 +131,8 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: applyBlock(rest) case _ => super.applyBlock(b) override def applyClsLikeDefn(cls: ClsLikeDefn): Unit = + cls.methodKeys.valuesIterator.foreach(applyPath) + cls.companion.foreach(_.methodKeys.valuesIterator.foreach(applyPath)) withClass(cls.isym): cls.parentPath.foreach(applyPath) applyBlock(cls.preCtor) @@ -261,7 +266,7 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: def operand(a: Arg)(using Raise, Scope): Document = if a.spread.nonEmpty then die else subexpression(a.value) - private def curriedFunctionBody(paramLists: Ls[ParamList], body: Block, generator: Bool): Block = + private def curriedFunctionBody(paramLists: Ls[LoweredParamList], body: Block, generator: Bool): Block = paramLists match case Nil => body case params :: Nil => @@ -357,8 +362,8 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: // lexical-`this` behavior of the Lambda IR. doc"(function* ($params) ${ braced(bodyDoc) }).bind(this)" else doc"($params) => ${ braced(bodyDoc) }" - case s @ SuperSelect(qual, base, id) => - doc"Reflect.get(${resultQual(base)}.prototype, ${makeStringLiteral(id.name)}, ${result(qual)})" + case s @ SuperSelect(qual, base, key) => + doc"Reflect.get(${resultQual(base)}.prototype, ${result(key)}, ${result(qual)})" case s @ Select(qual, id) => val checkCurrentSelection = checkSelections && s.sanitize val dotClass = s.symbol match @@ -379,10 +384,11 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: // * the access should throw an error like `TypeError: Cannot read property 'f' of undefined`. doc"$runtimeVar.checkSelect($sel, ${makeStringLiteral(id.name)}, $qualJS)" else sel - case DynSelect(qual, fld, ai) => - if ai - then doc"${resultQual(qual)}.at(${result(fld)})" - else doc"${result(qual)}[${result(fld)}]" + case DynSelect(qual, fld, kind) => kind match + case DynSelectKind.ArrayIdx => doc"${resultQual(qual)}.at(${result(fld)})" + // * `resultQual` parenthesizes a literal receiver, notably a negative number such as `-1`, + // * which would otherwise bind more loosely than the following bracket. + case DynSelectKind.Member(_) | DynSelectKind.Unknown => doc"${resultQual(qual)}[${result(fld)}]" case Instantiate(mut, cls, argss) => val calls = argss.foldLeft(resultInst(cls)): (acc, args) => doc"${acc}(${args.map(argument).mkDocument(", ")})" @@ -499,7 +505,7 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: case _ => N val name = symbolicSuffixBase(n.name).getOrElse(n.name) doc" # ${result(p)}${field.getOrElse(fieldSelect(name))} = ${result(r)};${returningTerm(rst, endSemi)}" - case AssignDynField(p, f, ai, r, rst) => + case AssignDynField(p, f, _, r, rst) => doc" # ${result(p)}[${result(f)}] = ${result(r)};${returningTerm(rst, endSemi)}" case Define(defn, rst) => inline def mkThis(sym: InnerSymbol): Document = @@ -562,7 +568,7 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: doc"${scope.lookup_!(sym, dSym.toLoc)} = (undefined, $functionKeyword ($params) ${ braced(bodyDoc) });" case ClsLikeDefn(ownr, isym, sym, ctorSym, kind, paramsOpt, auxParams, par, mtds, - privFlds, pubFlds, preCtor, ctor, modo, bufferable, termCompanion) + privFlds, pubFlds, preCtor, ctor, modo, bufferable, termCompanion, clsMethodKeys) => // After ClassParamFlattener, all classes have paramsOpt = N and exactly one auxParams entry. assert(paramsOpt.isEmpty, @@ -585,21 +591,22 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: case S(owner) => doc"${result(owner.asThis)}${selectPrivateField(isym, isym.toLoc).getOrElse(fieldSelect(sym.nme))}" case N => scope.lookup_!(sym, sym.toLoc) - def mkMethodName(td: FunDefn): Document = + def mkMethodName(td: FunDefn, keys: Map[TermSymbol, Path]): Document = if td.dSym.isPrivate then doc"#${isym.privatesScope.allocateOrGetName(td.dSym)}" - else doc"${td.sym.nme}" + else keys.get(td.dSym).fold(doc"${td.sym.nme}")( + key => outerScope.givenIn { doc"[${result(key)}]" }) - def mkMethods(mtds: Ls[FunDefn], mtdPrefix: Str)(using Scope): Document = + def mkMethods(mtds: Ls[FunDefn], mtdPrefix: Str, keys: Map[TermSymbol, Path])(using Scope): Document = mtds.map: case td @ FunDefn(params = ps :: pss, body = bod) => val result = curriedFunctionBody(pss, bod, td.generator) val (params, bodyDoc) = scope.nest.givenIn: setupFunction(S(td.sym.nme), ps, result, isLambda = false) val generatorPrefix = if td.generator && pss.isEmpty then "*" else "" - doc" # $mtdPrefix$generatorPrefix${mkMethodName(td)}($params) ${ braced(bodyDoc) }" + doc" # $mtdPrefix$generatorPrefix${mkMethodName(td, keys)}($params) ${ braced(bodyDoc) }" case td @ FunDefn(params = Nil, body = bod) => - doc" # ${mtdPrefix}get ${mkMethodName(td)}() ${ braced(body(bod, endSemi = true)) }" + doc" # ${mtdPrefix}get ${mkMethodName(td, keys)}() ${ braced(body(bod, endSemi = true)) }" .mkDocument(doc"") def mkPrivs(pubFlds: Ls[BlockMemberSymbol -> TermSymbol], privFlds: Ls[TermSymbol], @@ -666,7 +673,7 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: val ctorCode = if mod.ctor.isEmpty then doc"" else doc" # static " :: braced: body(mod.ctor, endSemi = true) privs :: ctorCode :: { - mkMethods(mod.methods, mtdPrefix) + mkMethods(mod.methods, mtdPrefix, mod.methodKeys) } // * Note that `thisProxy` might be defined at this point, // * if the module accesses the self-reference of an outer definition. @@ -690,7 +697,7 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: val ctorCode = scope.nest.givenIn: val preCtorCode = nonNestedScoped(preCtor)(bd => block(bd, true)) val defaultSuperCall = if par.isDefined && preCtor.isEmpty then doc" # super();" else doc"" - doc"$defaultSuperCall$preCtorCode$singletonInit${nonNestedScoped(ctor)(bd => block(bd, endSemi = true))}${ + doc"${normalizeOptionalParams(backendParamList)}$defaultSuperCall$preCtorCode$singletonInit${nonNestedScoped(ctor)(bd => block(bd, endSemi = true))}${ kind match case syntax.Obj => doc" # ${defineProperty(doc"this", "class", doc"${scope.lookup_!(isym, isym.toLoc)}")};" @@ -738,7 +745,7 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: .mkDocument(" ") else doc"" } :: { - mkMethods(mtds, mtdPrefix) + mkMethods(mtds, mtdPrefix, clsMethodKeys) } :: { // * If this class has a `toString` implementation, then delegate // * `prettyPrint` to `toString`. @@ -769,13 +776,14 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: // Source params are used for the wrapper to preserve the curried calling convention. // All args are forwarded to the flat `new Class.class(...)` constructor. val sourceAuxParams = isym.defn.map(_.auxParams).getOrElse(Nil) - val allSourceParams = sourceParamsOpt.toList ::: sourceAuxParams + val allSourceParams = + sourceParamsOpt.iterator.++(sourceAuxParams).map(LoweredParamList.fixed).toList val fun = allSourceParams match case ps_ :: pss_ if sourceParamsOpt.isDefined => outerScope.nest.givenIn: val (ps, _) = setupFunction(some(sym.nme), ps_, End(), isLambda = false) val pss = pss_.map(setupFunction(N, _, End(), isLambda = false)._1) - val argsDoc = allSourceParams.flatMap(_.paramSyms) + val argsDoc = allSourceParams.iterator.flatMap(_.paramSyms) .map(p => scope.lookup_!(p, p.toLoc)).mkDocument(", ") val inner = doc"new ${sym.nme}.class($argsDoc)" val bod = braced(doc" # return $freeze($inner);") @@ -1072,12 +1080,18 @@ class JSBuilder(using Config, TL, State, Ctx) extends CodeBuilder: (if enumerable then doc"enumerable: true, # " else doc"") :: doc"value: ${value}" })" - def setupFunction(name: Option[Str], params: ParamList, body: Block, isLambda: Bool) + private def normalizeOptionalParams(params: LoweredParamList)(using Raise, Scope): Document = + params.params.filter(_.flags.opt).map: p => + val name = scope.lookup_!(p.sym, p.toLoc) + doc" # $name ??= null;" + .mkDocument("") + + def setupFunction(name: Option[Str], params: LoweredParamList, body: Block, isLambda: Bool) (using Raise, Scope): (Document, Document) = val paramsList = params.params.map(p => scope.allocateName(p.sym)) .++(params.restParam.map(p => "..." + scope.allocateName(p.sym))) .mkDocument(", ") - (paramsList, this.body(body, endSemi = false)) + (paramsList, normalizeOptionalParams(params) :: this.body(body, endSemi = false)) @@ -1203,16 +1217,18 @@ trait JSBuilderArgNumSanityChecks(using TL, Config, Elaborator.State) val functionParamVarargSymbol = semantics.TempSymbol(N, erasedType = N, "args") - override def setupFunction(name: Option[Str], params: ParamList, body: Block, isLambda: Bool)(using Raise, Scope): (Document, Document) = + override def setupFunction(name: Option[Str], params: LoweredParamList, body: Block, isLambda: Bool)(using Raise, Scope): (Document, Document) = // * We used to instrument `fun f(x, y) = x + y` into something like // * `function f(...args) { runtime.checkArgs("f", 2, true, args.length); let x = args[0]; let y = args[1]; x + y }` // * which was very verbose, in addition to possibly making things quite inefficient. // * Now, we no longer instrument lambdas (which affects extra parameter lists), // * and we instead use the JS builtin `arguments` array to get the number of received arguments, as in - // * `function f(x, y) { runtime.checkArgs("f", 2, true, arguments.length); x + y }` + // * `function f(x, y) { runtime.checkArgs("f", 2, arguments.length); x + y }` // * The idea is that later on, we'll add a runtime type sanity check as well anyway, // * which will check arguments against the erased parameter type, // * including checking they are not `undefined`, which should achieve most of the benefit. + // * Following JavaScript, extra arguments are ignored, so we only check for missing arguments + // * (optional parameters are not counted in `paramCountLB`; missing optional arguments become `null`). /* if instrument then val paramsList = params.params.map(p => scope.allocateName(p.sym)) @@ -1227,13 +1243,11 @@ trait JSBuilderArgNumSanityChecks(using TL, Config, Elaborator.State) case S(p) => doc"\nlet $p = $runtimeVar.Tuple.slice($paramsStr, ${params.paramCountLB}, 0);" (doc"...$paramsStr", doc"$checkArgsNum$paramsAssign$restAssign${this.body(body, endSemi = false)}") */ + val (paramsList, bodyDoc) = super.setupFunction(name, params, body, isLambda) if instrument && !isLambda then val functionName = JSBuilder.makeStringLiteral(name.fold("")(n => s"${JSBuilder.escapeStringCharacters(n)}")) - val checkArgsNum = doc"\n$runtimeVar.checkArgs($functionName, ${params.paramCountLB}, ${params.paramCountUB.toString}, arguments.length);" - val paramsList = params.params.map(p => scope.allocateName(p.sym)) - .++(params.restParam.map(p => "..." + scope.allocateName(p.sym))) - .mkDocument(", ") + val checkArgsNum = doc"\n$runtimeVar.checkArgs($functionName, ${params.paramCountLB}, arguments.length);" (paramsList, - doc"$checkArgsNum${this.body(body, endSemi = false)}") + doc"$checkArgsNum$bodyDoc") else - super.setupFunction(name, params, body, isLambda = isLambda) + (paramsList, bodyDoc) 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 7d939c0d85..ec9c751fb4 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 @@ -10,7 +10,7 @@ import document.* import document.Document import semantics.{ BlockMemberSymbol, ClassSymbol, ClassLikeSymbol, ClassCtorSymbol, Elaborator, - InnerSymbol, LabelSymbol, LocalVarSymbol, ModuleOrObjectSymbol, ParamList, Symbol, TempSymbol, TermSymbol, + InnerSymbol, LabelSymbol, LocalVarSymbol, ModuleOrObjectSymbol, Symbol, TempSymbol, TermSymbol, }, Elaborator.State import text.Param as WasmParam @@ -510,7 +510,7 @@ object FunctionCtx: * When set, overrides the loaded type of each param slot at its position (index 0 = `this`). */ class FunctionCtx( - _params: Ls[ParamList], + _params: Ls[LoweredParamList], thisSym: Opt[InnerSymbol], paramTypes: Opt[Seq[ValType]] = N, )(using Ctx, Raise, State): @@ -617,10 +617,19 @@ end FunctionCtx * Returns the result of the `mkBody` function along with the [[FunctionCtx]]. */ def genFuncBody[T]( - params: Ls[ParamList], + params: Ls[LoweredParamList], thisSym: Opt[InnerSymbol], 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. + // * Variadic parameters still need argument packing: binding one as a plain parameter would + // * pass a single argument rather than an array of the remaining ones. + params.foreach: ps => + ps.restParam.foreach: p => + // * The rest parameter introduced when lowering trailing parameters has no location of its own. + 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 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 bc65e9a1f4..e0f10ee7bd 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 @@ -379,13 +379,20 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui private def castToValType(r: codegen.Result, target: ValType)(using FunctionCtx, Raise, SessionExportCtx): Expr = castConserve(result(r), target) - /** Casts each argument in `wasmArgs` down to the corresponding declared parameter type read from `funcTypeInfo`. */ - private def castArgsToParams(wasmArgs: Seq[Expr], funcTypeInfo: TypeInfo)(using Raise): Seq[Expr] = + /** 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] = val declParams = funcTypeInfo.asFunctionType_!.sigType.params - wasmArgs.zip(declParams).map: (arg, p) => - p.valtype match - case rt: RefType => castConserve(arg, rt) - case _ => arg + if wasmArgs.size > declParams.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) + 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)) /** Returns the default Wasm value for one struct field when eagerly constructing an object instance. */ private def defaultStructFieldValue(field: Field)(using Raise): Expr = field.ty.zeroValue.getOrElse: @@ -472,7 +479,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui ctorSym = N, k = syntax.Obj, paramsOpt = N, - auxParams = PlainParamList(Nil)(N) :: Nil, + auxParams = LoweredParamList.plain(Nil)(N) :: Nil, parentPath = N, methods = Nil, privateFields = Nil, @@ -482,6 +489,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui companion = N, bufferable = N, termCompanion = N, + methodKeys = Map.empty, )(N, Nil) /** Registers the synthetic `Unit` singleton. */ @@ -939,7 +947,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui /** Returns the single flattened constructor parameter list. After ClassParamFlattener, all classes have paramsOpt = N * and exactly one auxParams entry. */ - private def classCtorParamList(defn: ClsLikeDefn): ParamList = + private def classCtorParamList(defn: ClsLikeDefn): LoweredParamList = assert( defn.paramsOpt.isEmpty, s"WatBuilder: expected paramsOpt to be None after flattening for class ${defn.sym.nme}", @@ -1029,7 +1037,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui * Note that only top-level functions that will be lowered by the [[FunDefn]] arm of [[returningTerm]] can be * pre-declared with this function - see [[isPredeclarableTopLevelFun]]. */ - private def predeclareTopLevelFun(sym: BlockMemberSymbol, ps: ParamList)(using Raise): Unit = + private def predeclareTopLevelFun(sym: BlockMemberSymbol, ps: LoweredParamList)(using Raise): Unit = val params = ps.paramSyms.map(p => p -> SymIdx(p.nme)) val resultTypes = Seq(Result(declaredResultType(sym))) val funcTy = ctx.addType(TypeInfo( @@ -1416,7 +1424,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui case TryBlock(sub, finallyDo, _) => TryBlock(sub, finallyDo, rest) case Assign(lhs, rhs, _) => Assign(lhs, rhs, rest) case af @ AssignField(lhs, nme, rhs, _) => AssignField(lhs, nme, rhs, rest)(af.symbol) - case AssignDynField(lhs, fld, arrayIdx, rhs, _) => AssignDynField(lhs, fld, arrayIdx, rhs, rest) + case AssignDynField(lhs, fld, kind, rhs, _) => AssignDynField(lhs, fld, kind, rhs, rest) case Define(defn, _) => Define(defn, rest) case Match(scrut, arms, dflt, _) => Match(scrut, arms, dflt, rest) case Label(label, loop, body, _) => Label(label, loop, body, rest) @@ -1440,9 +1448,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 superCall = call( funcidx = ctx.getFunc_!(parentInitFunc), - operands = local.get(thisVar, RefType(parentTypeRef, nullable = false)) +: args.map(argument), + operands = adaptCallArgs(local.get(thisVar, RefType(parentTypeRef, nullable = false)) +: + args.map(argument), parentFuncType, clsLikeDefn.sym.toLoc), returnTypes = Seq(Result(RefType(parentTypeRef, nullable = false))), ) prefixWat :+ drop(superCall) @@ -1683,10 +1693,6 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui val baseSym = vt.slots(slot).owner val paramTypes = vt.slots(slot).paramTypes val resultType = vt.slots(slot).resultType - softAssert( - args.size == paramTypes.size, - s"Virtual call arity mismatch for $methodSym: ${args.size} args vs ${paramTypes.size} declared params", - ) val ownerTypeInfoIdx = typeInfoTypeIdxs(ownerCls) val receiverTmp = mkTempLocal("receiver", erasedType = N) val receiverExpr = local.set(receiverTmp, result(qual)) @@ -1701,7 +1707,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui ownerTypeInfoRef, RefType(virtualMethodTypeIdx, nullable = true), ) - val operands = castArgsToParams(receiverRef +: args.map(argument), ctx.getTypeInfo_!(virtualMethodTypeIdx)) + val operands = adaptCallArgs(receiverRef +: args.map(argument), ctx.getTypeInfo_!(virtualMethodTypeIdx), qual.toLoc) val virtualCall = call_ref( target = methodRef, operands = operands, @@ -1715,7 +1721,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui Ls(receiverExpr, adaptedCall) case N => val funcTypeInfo = ctx.getTypeInfo_!(ctx.getFuncTypeUse_!(methodSym).typeIdx) - val operands = castArgsToParams(result(qual) +: args.map(argument), funcTypeInfo) + val operands = adaptCallArgs(result(qual) +: args.map(argument), funcTypeInfo, qual.toLoc) Ls(call( funcidx = ctx.getFunc_!(methodSym), operands = operands, @@ -1737,6 +1743,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui operands = Seq(ref.i31(i32.const(lit.offset)), ref.i31(i32.const(lit.byteLen))), returnTypes = Seq(Result(RefType.anyref)), ) + case Value.Lit(UnitLit(true)) => ref.`null`(HeapType.Any) case Value.Lit(UnitLit(false)) => // `undefined` is the IR's canonical "no meaningful value", so it lowers to `$Unit` (similar to // `normalizeValueExprs`). @@ -1869,7 +1876,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 = castArgsToParams(args.map(argument), baseTypeInfo) + val wasmArgs = adaptCallArgs(args.map(argument), baseTypeInfo, c.toLoc) call( funcidx = baseFuncIdx, @@ -1879,7 +1886,6 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui case _ => val base = subexpression(fun) if base.resultTypes.exists(_ is UnreachableType) then return base - val wasmArgs = args.map(argument) val baseTypeIdx = base.resultType match case S(RefType(idx: TypeIdx, _)) => idx @@ -1897,6 +1903,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) call_ref( target = base, @@ -1969,9 +1976,9 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui extraInfo = S(sel), ) - case dyn @ DynSelect(qual, fld, arrayIdx) => + case dyn @ DynSelect(qual, fld, kind) => val qualRes = result(qual) - if arrayIdx then + if kind.isArrayIdx then val idxBuilder = compileTupleIndex( fld = fld, loc = fld.toLoc, @@ -2040,7 +2047,7 @@ 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 = castArgsToParams(as.map(argument), ctorTypeIdx) + val ctorArgs = adaptCallArgs(as.map(argument), ctorTypeIdx, r.toLoc) call(funcidx = ctorFuncIdx, operands = ctorArgs, Seq(Result(RefType(ctorClsTypeIdx, nullable = false)))) case Tuple(mut, elems) => @@ -2201,11 +2208,11 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui val rstBlk = returningTerm(rst) assignInstr +: rstBlk - case assign @ AssignDynField(lhs, fld, arrayIdx, rhs, rst) => + case assign @ AssignDynField(lhs, fld, kind, rhs, rst) => val lhsExpr = result(lhs) val rhsExpr = result(rhs) val assignInstr = - if arrayIdx then + if kind.isArrayIdx then val tupleArrayType = this.tupleArrayType(mut = true) val tupleRef = ref.cast(lhsExpr, RefType(tupleArrayType, nullable = false)) val idxBuilder = compileTupleIndex( @@ -2218,7 +2225,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui array.set(tupleArrayType, tupleRef, idxExpr, rhsExpr) else errExpr( - Ls(msg"WatBuilder::returningTerm for AssignDynField(...) where `arrayIdx = false` is not implemented yet" -> + Ls(msg"WatBuilder::returningTerm for AssignDynField(...) with a non-index key is not implemented yet" -> lhs.toLoc), extraInfo = S(assign), ) @@ -2401,7 +2408,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui def overwriteMethod( sym: BlockMemberSymbol, - ps: ParamList, + ps: LoweredParamList, bod: Block, ): Unit = val paramTypes = ctx.getVirtualTable(clsLikeDefn.sym).flatMap: vt => @@ -2428,7 +2435,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui clsLikeDefn.methods.foreach: case FunDefn(_, sym, _, Nil, bod) => - overwriteMethod(sym, PlainParamList(Nil)(N), bod) + overwriteMethod(sym, LoweredParamList.plain(Nil)(N), bod) case FunDefn(_, sym, _, ps :: Nil, bod) => overwriteMethod(sym, ps, bod) case methodDefn => @@ -2635,6 +2642,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui val scrutAsI31 = ref.cast(getScrutExpr, RefType.i31ref) val scrutValue = i31.get(scrutAsI31, signed = true) i32.eq(scrutValue, withValidIntLit(value, lit.toLoc)(i32.const)) + case UnitLit(true) => ref.is_null(getScrutExpr) case _ => break(errExpr(Ls(msg"Pattern matching for unit literals not implemented yet" -> lit.toLoc))) @@ -2968,7 +2976,7 @@ class WatBuilder(private val ctx: Ctx)(using TraceLogger, State) extends CodeBui def setupFunction( thisSym: Opt[InnerSymbol], - params: ParamList, + params: LoweredParamList, body: Block, paramTypes: Opt[Seq[ValType]] = N, )(using Raise, SessionExportCtx): (Expr, FunctionCtx) = diff --git a/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala b/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala index 5aaba70cb4..519ed70bb6 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/invalml/InvalML.scala @@ -140,7 +140,9 @@ class InvalTyper(using codegen.Erasure)(using elState: Elaborator.State, tl: TL) error(msg"Variable not found: ${sym.nme}" -> ty.toLoc :: Nil) case FunTy(Term.Tup(params), ret, eff) => PolyFunType(params.map { - case Fld(_, p, _) => typeAndSubstType(p, !pol) + case Fld(flags, p, _) => + if flags.opt then error(msg"Optional parameters are not supported by InvalML." -> p.toLoc :: Nil) + typeAndSubstType(p, !pol) case spd: Spd => lastWords(s"unexpected spread in function type parameters: $spd") }, typeAndSubstType(ret, pol), eff.map(e => typeAndSubstType(e, pol) match { case t: Type => t @@ -434,8 +436,8 @@ class InvalTyper(using codegen.Erasure)(using elState: Elaborator.State, tl: TL) case _ => error(msg"Cannot match ${scrutinee.toString} as ${sym.toString}" -> split.toLoc :: Nil) (Bot, Bot, Bot) - scrutinee match // * refine - case Ref(sym: LocalSymbol) => + scrutinee.sym match // * refine + case sym: LocalSymbol => nestCtx1 += sym -> clsTy nestCtx2 += sym -> tv case _ => () // TODO: refine all variables holding this value? @@ -626,7 +628,8 @@ class InvalTyper(using codegen.Erasure)(using elState: Elaborator.State, tl: TL) case sel @ Term.SynthSel(Ref(_: TopLevelSymbol), nme) if sel.symbol.isDefined => typeCheck(Ref(sel.symbol.get)(sel.nme, N)) - case Ref(sym) => + case ref: (Ref | SimpleRef) => + val sym = ref.sym ctx.get(sym) match case Some(ty) => (ty, Bot) case _ => @@ -702,7 +705,7 @@ class InvalTyper(using codegen.Erasure)(using elState: Elaborator.State, tl: TL) ty } constrain(tryMkMono(ty, term), ClassLikeType(clsSym, targs)) - require(clsDfn.paramsOpt.forall(_.restParam.isEmpty)) + require(clsDfn.paramsOpt.forall(_.variadic.isEmpty)) (clsDfn.paramsOpt.fold(Nil)(_.params).map { case Param(_, sym, sign, _) => if sym.nme === field.name then sign else N @@ -717,7 +720,7 @@ class InvalTyper(using codegen.Erasure)(using elState: Elaborator.State, tl: TL) case Term.New(cls, args, N) => cls.symbol.flatMap(_.asCls.flatMap(_.defn)) match case S(clsDfn: ClassDef.Parameterized) => - require(clsDfn.paramsOpt.forall(_.restParam.isEmpty)) + require(clsDfn.paramsOpt.forall(_.variadic.isEmpty)) val argsList = args match case Nil => Nil case Term.Tup(elems) :: Nil => elems.map: @@ -740,7 +743,7 @@ class InvalTyper(using codegen.Erasure)(using elState: Elaborator.State, tl: TL) ty } val effBuff = ListBuffer.empty[Type] - require(clsDfn.paramsOpt.forall(_.restParam.isEmpty)) + require(clsDfn.paramsOpt.forall(_.variadic.isEmpty)) argsList.iterator.zip(clsDfn.params.params).foreach { case (arg, Param(sign = S(sign))) => val (ty, eff) = ascribe(arg, typeAndSubstType(sign, pol = true)(using map.toMap)) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/AccessPath.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/AccessPath.scala new file mode 100644 index 0000000000..c2586e487d --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/AccessPath.scala @@ -0,0 +1,103 @@ +package hkmc2 +package semantics + +import hkmc2.utils.*, shorthands.* +import Elaborator.State +import Term.* +import syntax.Tree + +/** Recover an expression for an implicit reference from its use site's lexical + * scope. As with companion-module references in the legacy flow resolver, a + * module can supply a path to its members, but a function or local block cannot. + * Imported file bindings use the existing compilation-unit linkage. + */ +object AccessPath: + enum Failure: + case OutOfScope + case PrivateMember(member: DefinitionSymbol[?]) + + private def checked(target: DefinitionSymbol[?])(make: => Term)(using state: State): Either[Failure, Term] = + if (target.getState isnt state) && codegen.MemberStorage.sourcePrivateDefinition(target).nonEmpty + then L(Failure.PrivateMember(target)) + else R(make) + + private def reference(target: DefinitionSymbol[?])(using State): Either[Failure, Term] = checked(target): + val member = target.bms.getOrElse(lastWords("An access path requires a block member")) + val ref = MemberRef(member)(N, FlowSymbol.memSym(member)) + ref.resolvedTargets = target :: Nil + ref + + private def select(prefix: Term, target: DefinitionSymbol[?])(using State): Either[Failure, Term] = checked(target): + val member = target.bms.getOrElse(lastWords("An access path requires a block member")) + val sel = new NewSel(prefix, Tree.Ident(member.nme), N)(FlowSymbol.synthSel(member.nme)) + sel.resolvedTargets = target :: Nil + sel.resolvedMembers = member :: Nil + sel + + /** Recover a path to `target` from `src` by walking its enclosing module/object owners + * until reaching a binding available in `src`. For a member declared in [scp2] here, + * [scp1] is the outermost accessible base: + * {{{ + * [scp0] + * fun foo = + * [scp1] + * module Foo with + * module Bar with + * [scp2] + * }}} + * From [scp1], the path to that member is `Foo.Bar.member`. [scp0] cannot access + * [scp2] because there is a function scope on the way: the owner walk cannot + * obtain the local binding of `Foo` from outside an invocation of `foo`. + * The same would happen for: + * {{{ + * [scp0] + * if ... then // LocalScope also blocks access to [scp1] and [scp2] + * [scp1] + * module Foo with + * module Bar with + * [scp2] + * }}} + * This restriction prevents entering an inaccessible scope; it does not prevent + * nested functions from capturing bindings in their enclosing scopes. `encloses` + * recognizes those bindings before the owner walk is needed. Imported file bindings + * and global declarations can also provide the base of a path directly. + */ + def findAccessPath(src: SrcScope, target: DefinitionSymbol[?])(using state: State): Either[Failure, Term] = + def encloses(dst: SrcScope): Bool = + def loop(scope: SrcScope): Bool = (scope is dst) || scope.parent.exists(loop) + loop(src) + val scope = target.defn.flatMap: + case td: TermDefinition => td.scope + case md: ModuleOrObjectDef => S(md.path) + case _ => N + val imported = target.bms.exists: sym => + sym.getState.externalModuleImport(sym, state) match + case S(_: Elaborator.ExternalModuleImport.Default) => true + case S(_: Elaborator.ExternalModuleImport.Private) => sym.getState.compilationUnitPrivateName(sym).nonEmpty + case N => false + val global = scope.exists(_.outer.inner.isEmpty) && target.defn.exists(_.hasDeclareModifier.nonEmpty) + if global || imported then reference(target) + else scope.toRight(Failure.OutOfScope).flatMap: dst => + // Different files may share a prelude scope, but only their exported + // bindings are available to one another, not their lexical receivers. + if (target.getState is state) && encloses(dst) then dst.outer.inner match + case S(owner) => select(SelfRef(owner)(Tree.Ident(owner.nme)), target) + case N => reference(target) + else dst.outer.inner match + case S(owner: ModuleOrObjectSymbol) => findAccessPath(src, owner).flatMap(select(_, target)) + case _ => L(Failure.OutOfScope) + + /** The key's symbol identity remains unchanged. Only the runtime expression is + * recovered here, after member resolution has finished; it adds no flow edges. + */ + def symbolKey(src: SrcScope, key: SymbolKey)(using State): Either[Failure, Term] = + def target(sym: Symbol, receiver: Bool): DefinitionSymbol[?] = sym match + case bms: BlockMemberSymbol => + (if receiver then bms.asMod.orElse(bms.asTrm) else bms.asTrm).getOrElse( + lastWords("A validated symbol path must denote a module or immutable val")) + case sym: TermSymbol => sym + case sym: ModuleOrObjectSymbol => sym + case _ => lastWords("Invalid symbol path root") + key.path.tail.zipWithIndex.foldLeft(findAccessPath(src, target(key.path.head, key.path.tail.nonEmpty))): (prefix, entry) => + val (sym, index) = entry + prefix.flatMap(select(_, target(sym, index < key.path.length - 2))) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala index 5dbafa3ab2..98e1c6dbf0 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Elaborator.scala @@ -110,7 +110,8 @@ object Elaborator: val none: UnderCtx = UnderCtx(N) case class Ctx( - outer: OuterCtx, + // Copies that update names preserve lexical identity; only nest creates a scope. + scope: SrcScope, parent: Opt[Ctx], env: Map[Str, Ctx.Elem], mode: Mode, @@ -120,7 +121,7 @@ object Elaborator: override def toString: Str = s"${parent.fold("")(_.toString+"/")}${outer.showDbg}" - lazy val scope: SrcScope = SrcScope(outer, parent.map(_.scope)) + def outer: OuterCtx = scope.outer /** A nominal member can refer to enclosing binders even when its own * signature does not mention them, for example through a local type alias. @@ -165,7 +166,8 @@ object Elaborator: labelSym, resultSym, nonLocalHandlerSym, nonLocalBreakMethodMarker, nonLocalContinueMethodMarker)) ) - def nest(outerCtx: OuterCtx): Ctx = Ctx(outerCtx, Some(this), Map.empty, mode, Map.empty, Nil) + def nest(outerCtx: OuterCtx): Ctx = + Ctx(SrcScope(outerCtx, S(scope)), Some(this), Map.empty, mode, Map.empty, Nil) def nestLocal(nameHint: Str): Ctx = nest(OuterCtx.LocalScope(nameHint)) def nestInner(inner: InnerSymbol): Ctx = nest(OuterCtx.InnerScope(inner)) @@ -189,6 +191,16 @@ object Elaborator: private def capture(elem: Ctx.Elem): Ctx.Elem = outer.resolutionBoundary.fold(elem)(Ctx.CaptElem(elem, _)) + /** Control handlers have no source name, but references to them cross the same + * lexical boundaries as named bindings. In particular, a return inside a + * nested class must transport its payload back out of that class. */ + def controlHandler(symbol: TempSymbol): Opt[Ctx.Elem] = + val owns = outer match + case OuterCtx.Function(handler, _) => handler is symbol + case _ => false + if owns || labels.valuesIterator.exists(_.nonLocalHandlerSymbol is symbol) then S(Ctx.RefElem(symbol)) + else parent.flatMap(_.controlHandler(symbol)).map(capture) + private lazy val visibleWildcardOpens: Ls[Ctx.OpenSource] = val inherited = parent.toList.flatMap(_.visibleWildcardOpens) wildcardOpens ::: inherited.map(source => Ctx.OpenSource(capture(source.elem))(source.id)) @@ -309,7 +321,7 @@ object Elaborator: val Object = assumeBuiltinCls("Object") val Array = assumeBuiltinCls("Array") val TypedArray = assumeBuiltinCls("TypedArray") - val Symbol = assumeBuiltinCls("Symbol") + lazy val Symbol = assumeBuiltinCls("Symbol") // println(s"Builtins: $Int, $Num, $Str, $untyped") class VirtualModule private(val module: ModuleOrObjectSymbol, val bms: BlockMemberSymbol): def this(module: ModuleOrObjectSymbol) = @@ -356,6 +368,8 @@ object Elaborator: val line = assumeObject("line") val name = assumeObject("name") val file = assumeObject("file") + object unsafe extends VirtualModule(assumeBuiltinMod("unsafe")): + val undefined = assumeObject("undefined") object js extends VirtualModule(assumeBuiltinMod("js")): val bitand = assumeObject("bitand") val bitnot = assumeObject("bitnot") @@ -400,6 +414,8 @@ object Elaborator: object debug extends VirtualModule(assumeBuiltinMod("debug")): val printStack = assumeObject("printStack") object annotations extends VirtualModule(assumeBuiltinMod("annotations")): + lazy val fresh = assumeObject("fresh") + lazy val jsSymbol = assumeObject("jsSymbol") val untyped = assumeObject("untyped") val tailrec = assumeObject("tailrec") val tailcall = assumeObject("tailcall") @@ -448,20 +464,20 @@ object Elaborator: abstract class Elem: def nme: Str - def ref(id: Ident)(using Elaborator.State, Ctx, Config, NewResolver, ResolutionContext): Term + def ref(id: Opt[Ident])(using Elaborator.State, Ctx, Config, NewResolver, ResolutionContext): Term def symbol: Opt[Symbol] def isImport: Bool final case class RefElem(sym: RefSymbol) extends Elem: val nme = sym.nme - def ref(id: Ident)(using Elaborator.State, Ctx, Config, NewResolver, ResolutionContext): Term = - // * Note: due to symbolic ops, we may have `id.name =/= nme`; - // * e.g., we can have `id.name = "|>"` and `nme = "pipe"`. + def ref(id: Opt[Ident])(using Elaborator.State, Ctx, Config, NewResolver, ResolutionContext): Term = + // * A source identifier can differ from the symbol name due to symbolic + // * operators, e.g., source spelling "|>" for the symbol named "pipe". if config.language.useNewResolution then sym match case sym: SimpleRefSymbol => Term.SimpleRef(sym)(id) case sym: InnerSymbol => - Term.SelfRef(sym)(id) + Term.SelfRef(sym)(id.getOrElse(Ident(sym.nme))) case sym: MemberSymbol => // A reference can invoke a by-name definition, instantiating its // binders at this site (see NewResolver.instantiateByName). @@ -470,16 +486,17 @@ object Elaborator: rctx.recordAccessSite(ref, ctx.scope) ref else - sym.ref(id) + sym.ref(id.getOrElse(Ident(sym.nme))) def symbol = S(sym) def isImport: Bool = false final case class SelElem(base: Elem, nme: Str, symOpt: Opt[MemberSymbol], isImport: Bool) extends Elem: - def ref(id: Ident)(using Elaborator.State, Ctx, Config, NewResolver, ResolutionContext): Term = + def ref(id: Opt[Ident])(using Elaborator.State, Ctx, Config, NewResolver, ResolutionContext): Term = // * Same remark as in RefElem#ref - val prefix = base.ref(Ident(base.nme)) - val name = new Ident(nme).withLocOf(id) + val prefix = base.ref(N) + val loc = id.flatMap(_.toLoc) + val name = new Ident(nme).withLoc(loc) if config.language.useNewResolution then - val res = new Term.NewSel(prefix, name, N)(FlowSymbol.synthSel(nme)).withLocOf(id) + val res = new Term.NewSel(prefix, name, N)(FlowSymbol.synthSel(nme)).withLoc(loc) rctx.recordLexicalBinders(res, ctx.typeBinders) rctx.recordAccessSite(res, ctx.scope) summon[NewResolver].newSel(res, ctx.scope) @@ -488,11 +505,12 @@ object Elaborator: Term.SynthSel(prefix, name)(symOpt, FlowSymbol.synthSel(nme), N, S(summon)) def symbol = symOpt final case class WildcardElem(nme: Str, sources: Ls[OpenSource]) extends Elem: - def ref(id: Ident)(using Elaborator.State, Ctx, Config, NewResolver, ResolutionContext): Term = + def ref(id: Opt[Ident])(using Elaborator.State, Ctx, Config, NewResolver, ResolutionContext): Term = // Each ref is fresh. Locate its outer capture at the opening site too; // a chained wildcard reference may already have attached this location. - val prefixes = sources.map(source => source.elem.ref(source.id).withoutLoc.withLocOf(source.id)) - val res = new Term.UnresolvedRef(prefixes, id)(FlowSymbol.synthSel(nme)).withLocOf(id) + val prefixes = sources.map(source => source.elem.ref(S(source.id)).withoutLoc.withLocOf(source.id)) + val name: Ident = id.getOrElse(Ident(nme)) + val res = new Term.UnresolvedRef(prefixes, name)(FlowSymbol.synthSel(nme)).withLocOf(name) rctx.recordLexicalBinders(res, ctx.typeBinders) rctx.recordAccessSite(res, ctx.scope) summon[NewResolver].unresolvedRef(res, ctx.scope) @@ -500,13 +518,15 @@ object Elaborator: def symbol: Opt[Symbol] = N def isImport: Bool = true final case class CaptElem(base: Elem, thru: DefinitionSymbol[?]) extends Elem: - def ref(id: Ident)(using Elaborator.State, Ctx, Config, NewResolver, ResolutionContext): Term = + def ref(id: Opt[Ident])(using Elaborator.State, Ctx, Config, NewResolver, ResolutionContext): Term = Term.Capture(base.ref(id), thru) def symbol = base.symbol def isImport: Bool = false def nme: Str = base.nme given Conversion[RefSymbol, Elem] = RefElem(_) - val empty: Ctx = Ctx(OuterCtx.LocalScope("top-level"), N, Map.empty, Mode.Full, Map.empty, Nil) + val empty: Ctx = + val outer = OuterCtx.LocalScope("top-level") + Ctx(SrcScope(outer, N), N, Map.empty, Mode.Full, Map.empty, Nil) /** The interpretation required by the enclosing syntax. Class and pattern * targets are resolved by their enclosing construct; types must not request @@ -531,12 +551,14 @@ object Elaborator: transparent inline def ctx(using Ctx): Ctx = summon final case class RuntimeSymbols( + module: BlockMemberSymbol, unit: ModuleOrObjectSymbol, loopEnd: ModuleOrObjectSymbol, tuple: ModuleOrObjectSymbol, str: ModuleOrObjectSymbol, unreachable: TermSymbol, assertFail: TermSymbol, + checkVariadicArgs: TermSymbol, continue: ModuleOrObjectSymbol, toJsAsync: TermSymbol, tupleGet: TermSymbol, @@ -559,10 +581,9 @@ object Elaborator: case L(errs) => lastWords: errs.map(_.mainMsg).mkString("\n") + val module = topLevelMembers.getOrElse("Runtime", lastWords("Runtime.mls does not define module 'Runtime'.")) val members = - topLevelMembers - .get("Runtime") - .flatMap(_.asMod) + module.asMod .flatMap(_.defn) .fold(lastWords("Runtime.mls does not define module 'Runtime'."))(_.body.members) def member(name: Str): BlockMemberSymbol = @@ -582,12 +603,14 @@ object Elaborator: val tuple = modOrObj("Tuple") val str = modOrObj("StrOps") RuntimeSymbols( + module = module, unit = modOrObj("Unit"), loopEnd = modOrObj("LoopEnd"), tuple = tuple, str = str, unreachable = term("unreachable"), assertFail = term("assertFail"), + checkVariadicArgs = term("checkVariadicArgs"), continue = modOrObj("Continue"), toJsAsync = term("toJsAsync"), tupleGet = moduleMember(tuple, "get"), @@ -667,6 +690,7 @@ object Elaborator: private def runtimeSymbols: RuntimeSymbols = cachedRuntimeSymbols.getOrElse: lastWords("Runtime symbols have not been initialized from Runtime.mls.") + def runtimeModuleSymbol: BlockMemberSymbol = runtimeSymbols.module def unitSymbol: ModuleOrObjectSymbol = runtimeSymbols.unit // Stable symbol for the synthetic Wasm Unit singleton val unitBlockMemberSymbol = BlockMemberSymbol("Unit", Nil) @@ -697,6 +721,7 @@ object Elaborator: BlockMemberSymbol(id.name, Nil, true) def unreachableSymbol: TermSymbol = runtimeSymbols.unreachable def assertFailSymbol: TermSymbol = runtimeSymbols.assertFail + def checkVariadicArgsSymbol: TermSymbol = runtimeSymbols.checkVariadicArgs def continueSymbol: ModuleOrObjectSymbol = runtimeSymbols.continue def toJsAsyncSymbol: TermSymbol = runtimeSymbols.toJsAsync def tupleGetSymbol: TermSymbol = runtimeSymbols.tupleGet @@ -802,6 +827,9 @@ extends Importer: val lang = config.language val newResolution: Bool = lang.useNewResolution + private def syntheticLocalRef(sym: LocalVarSymbol): Term = + if newResolution then Term.SimpleRef(sym)(N) else sym.ref() + private def debugElaboration[T](annotations: Ls[Annot])(thunk: => T): T = tl.traceDefinition(annotations, config)(thunk) @@ -856,6 +884,15 @@ extends Importer: symbol match case S(sym) => sym match + case ctx.builtins.annotations.fresh => + return S(Annot.Fresh()(tree.toLoc)) + case ctx.builtins.annotations.jsSymbol => + return trm.withoutCaptures match + case Term.App(_, Term.Tup(Fld(_, path, N) :: Nil)) => + symbolKey(path, required = true).map(Annot.JsSymbol(_)(tree.toLoc)) + case _ => + raise(ErrorReport(msg"@jsSymbol requires one stable path to a val marked @annotations.fresh." -> tree.toLoc :: Nil)) + N case ctx.builtins.annotations.untyped => return S(Annot.Untyped()(tree.toLoc)) case ctx.builtins.annotations.tailcall => @@ -888,7 +925,7 @@ extends Importer: private def requireEffectMethodValue(methodName: Str, valueSym: Opt[VarSymbol]): Term = valueSym match - case S(sym) => sym.ref(Ident("value")) + case S(sym) => syntheticLocalRef(sym) case N => lastWords(s"Missing value parameter for non-local effect handler method '$methodName'.") /** Mark a handler method as used via symbol direct references, without emitting code. */ @@ -896,9 +933,13 @@ extends Importer: methodMarker.ref(callSiteId) () - /** Use a synthesized selection so the sentinel object can be referenced without field-access sanity checks. */ + /** Give the synthetic import the same module shape as an ordinary import alias, + * so compiler-inserted selections use normal member resolution and lowering. */ private def nonLocalContinueSentinel(using Ctx): Term = - State.runtimeSymbol.ref().synthSel(State.continueSymbol) + if newResolution then + pipeTerm(Ctx.RefElem(State.runtimeModuleSymbol).ref(N), State.runtimeSymbol) + Ctx.SelElem(Ctx.RefElem(State.runtimeSymbol), "Continue", S(State.continueSymbol), false).ref(N) + else State.runtimeSymbol.ref().synthSel(State.continueSymbol) /** Build an effect handler around `body` for non-local control flow. */ private def mkEffectHandleAbortive( @@ -906,13 +947,15 @@ extends Importer: effectClassName: Str, methods: Ls[EffectHandlerMethodSpec], body: Term, - )(using State): Term = + )(using State, Ctx): Term = val clsSym = ClassSymbol(DummyTypeDef(Cls), Ident(effectClassName)) + recordDefinitionScope(clsSym) val htds = methods.map: spec => val valueSym = spec.valueParamName.map(nme => VarSymbol(Ident(nme), erasedType = N)) val resumeSym = VarSymbol(Ident("resume"), erasedType = N) val mtdSym = BlockMemberSymbol(spec.methodName, Nil, true) - val tsym = TermSymbol(Fun, N, Ident(spec.methodName)) + val tsym = TermSymbol(Fun, S(clsSym), Ident(spec.methodName)) + rctx.state.lexicalTypeBinders(tsym) = ctx.typeBinders val td = TermDefinition( Fun, mtdSym, @@ -921,15 +964,20 @@ extends Importer: N, N, S(spec.methodBody(valueSym)), - TermDefFlags.empty, + TermDefFlags.empty.copy(isMethod = true), Modulefulness.none, Nil, N, - )(N, N) + )(N, N, N) mtdSym.tsym = S(tsym) tsym.defn = S(td) + mtdSym.complete() HandlerTermDefinition(resumeSym, td) - Term.Handle(handlerSymbol, state.nonLocalRetHandlerTrm, Nil, clsSym, htds, body) + clsSym.defn = S(ClassDef.Plain(ctx.getOuter, Cls, clsSym, BlockMemberSymbol(effectClassName, Nil), Nil, N, + ObjBody(Blk(htds.map(_.td), unit)), N, Nil, Nil, N, ctorAnnotations = Nil)(body.toLoc, body.toLoc, N)) + val handle = new Term.Handle(handlerSymbol, state.nonLocalRetHandlerTrm, Nil, clsSym, htds, body)(isAbortive = true) + if newResolution then resolveAbortiveHandler(handle) + handle /** Build a non-local effect invocation on `handlerSymbol`. */ private def mkNonLocalEffectInvocation( @@ -942,9 +990,18 @@ extends Importer: val rs = FlowSymbol.app() val mtdTree = new Ident(methodName) val argTree = new Tup(argTrees) - app( - Term.Sel(handlerSymbol.ref(callSiteId), mtdTree)( - S(state.nonLocalRet), FlowSymbol.sel(callSiteId.name), N, S(summon)), + val selection = if newResolution then + markEffectMethodUsed(handlerSymbol, callSiteId) + val receiver = ctx.controlHandler(handlerSymbol).getOrElse( + lastWords("A non-local control handler must belong to an enclosing scope")).ref(N) + val sel = withTypeBinders(new Term.NewSel(receiver, mtdTree.withLocOf(callSiteId), N)( + FlowSymbol.sel(callSiteId.name)).withLocOf(callSiteId)) + rctx.recordAccessSite(sel, ctx.scope) + newSel(sel, ctx.scope) + sel + else Term.Sel(handlerSymbol.ref(callSiteId), mtdTree)( + S(state.nonLocalRet), FlowSymbol.sel(callSiteId.name), N, S(summon)) + app(selection, withTypeBinders(Term.Tup(argTerms.map(term => PlainFld(term)))(argTree)), )(Tree.DummyApp, N, rs) @@ -1029,7 +1086,7 @@ extends Importer: /** Elaborate `case` expressions */ protected def caseSplit(scrut: VarSymbol, tree: Case): Ctxl[Term.IfLike] = val (form, split) = withScopedConnectives(tree.kw): - patternBranch(() => scrut.ref(), tree.branches, identity) + patternBranch(() => syntheticLocalRef(scrut), tree.branches, identity) ifLike(tree.kw.kw, form, split, tree.toLoc) protected def mkSplitLet(binding: LocalVarSymbol, term: Term): Head.Let = @@ -1042,8 +1099,7 @@ extends Importer: // Captures carry the scope crossings needed to resolve extracted bindings, // but matching the value at runtime only needs the underlying reference. val ref = scrutinee.withoutCaptures match - case ref: Term.Ref => ref - case ref: (Term.SimpleRef | Term.SelfRef) => ref.sym.ref().withLocOf(scrutinee) + case ref: (Term.Ref | Term.SimpleRef) => ref case _ => lastWords("A pattern scrutinee must be a reference") Head.Match(ref, pattern, consequent) @@ -1165,7 +1221,7 @@ extends Importer: // * Each branch scrutinee `rhs` is elaborated as the tree `lhs op rhs`, as in the non-split form, // * so that, eg, the RHS of `|>` delimits underscore lambdas (see `underscoreDelimitingSides`). case OpApp(lhs, ident: Ident, rhss) => - val lhsTerm = if underscoreDelimitingSides(ident)._1 then delimitedTerm(lhs, Trm) else term(lhs, Trm) + val lhsTerm = if underscoreDelimitingSides(ident)._1 then delimitedTerm(lhs, Trm, alwaysLambda = false) else term(lhs, Trm) val split = lhsTerm.reference: lhs => termSplit(rhss, ((rhs: Tree) => OpApp(Tree.Trm(lhs()), ident, rhs :: Nil)) andThen mk) (ctx, split) @@ -1226,7 +1282,7 @@ extends Importer: // Member references can invoke getters; evaluate those only once. case _ => val symbol = TempSymbol(N, erasedType = N, "scrut") - mkSplitLet(symbol, term) ~: continuation(() => symbol.ref()) + mkSplitLet(symbol, term) ~: continuation(() => syntheticLocalRef(symbol)) private type TT = (Tree, Tree) @@ -1273,16 +1329,18 @@ extends Importer: trace[Term](dbg"Elab term ${tree.showDbg}", r => s"~> ${r.showDbg}"): given UnderCtx = UnderCtx.none subterm(tree, interp) - else delimitedTerm(tree, interp) + else delimitedTerm(tree, interp, alwaysLambda = false) /** Elaborate `tree` as a term that delimits an implicit lambda, whose parameters are the `_` placeholders - * of `tree` that are not delimited by one of its subterms, as in the body of `=> _ + 1`. */ - def delimitedTerm(tree: Tree, interp: Interpretation): Ctxl[Term] = + * of `tree` that are not delimited by one of its subterms, as in the body of `=> _ + 1`. + * An explicit prefix `=>` always creates a lambda; implicit delimiters leave + * placeholder-free expressions eager. */ + def delimitedTerm(tree: Tree, interp: Interpretation, alwaysLambda: Bool): Ctxl[Term] = trace[Term](dbg"Elab term ${tree.showDbg}", r => s"~> ${r.showDbg}"): val unders = mutable.ArrayBuffer.empty[VarSymbol] given UnderCtx = UnderCtx(S(unders)) val st = subterm(tree, interp) - if unders.isEmpty then st + if unders.isEmpty && !alwaysLambda then st else val params = unders.iterator.map(Param.simple).toList withTypeBinders(Term.Lam(PlainParamList(params)(N), st)) @@ -1319,7 +1377,8 @@ extends Importer: */ private def recordFieldScope(field: RcdField)(using Ctx): RcdField = rctx.state.lexicalTypeBinders(field.tsym) = ctx.typeBinders - field + val key = field.key.orElse(symbolKey(field.field, required = false)) + field.copy()(key) /** An async expression wraps the values of `target` in a promise, whose payload * reads the binders in scope where the expression is written (see @@ -1334,8 +1393,7 @@ extends Importer: if newResolution then listenTerm(lt)(shape => appShape(shape, rt, res)) res - /** With the new underscore syntax, some operator operands delimit their own implicit lambdas, - * as if prefixed by `=>`: + /** With the new underscore syntax, some operator operands delimit their own implicit lambdas: * - the RHS of an operator whose name ends with `>` (such as `|>` and `>>`), * so that `x |> _ + 1` means `x |> (y => y + 1)`; * - the LHS of an operator whose name starts with `<` (such as `<|` and `<<`), @@ -1353,11 +1411,14 @@ extends Importer: * which is also what the operator split of `f` on lines `\ a` and `\ b` means, * as it is elaborated as `(f \ a) \ b` (`\` being otherwise right-associative). * Parentheses are not transparent, as they delimit subexpressions: `(f \ a) \ b` means `f(a)(b)`. - * With the new underscore syntax, the argument delimits its own implicit lambda, as if prefixed by `=>`, + * With the new underscore syntax, the argument delimits its own implicit lambda, * so that `xs.map \ _ + 1` means `xs.map(x => x + 1)`; this is done here rather than in * `Tree.desugared`, which does not know the language version. */ - private def backslashApp(lhs: Tree, rhs: Tree): Tree.App = - def delimited(value: Tree): Tree = PrefixApp(new Keywrd(Keyword.`=>`), value).withLocOf(value) + private def backslashApp(lhs: Tree, rhs: Tree)(using Ctx): Tree.App = + // These delimiters are implicit: synthesizing a prefix `=>` would also + // suspend arguments without placeholders instead of passing their values. + def delimited(value: Tree): Tree = + Tree.Trm(delimitedTerm(value, Trm, alwaysLambda = false)).withLocOf(value) // * Field-shaped arguments, such as `key: a` and `...xs` (see `fld`), are not expressions, // * so only their values delimit implicit lambdas. val arg = @@ -1414,6 +1475,37 @@ extends Importer: def error = Term.Error().withLocOf(tree) + /** Elaborate the parameters of a function type. Unlike other tuples, these may be marked optional, + * as in `(Int, Int?) -> Int`, in which case the parameter type also includes `null`, + * or droppable, as in `(Int, drop Int) -> Int`. */ + def funTyParams(lhs: Tree): Term = lhs match + case tree @ Tup(fields) => + val namedElems = fields.map: + case PrefixApp(Keywrd(Keyword.`drop`), field) => fld(field, Tpe) match + case f: Fld => f.copy(flags = f.flags.copy(droppable = true)) + case spd: Spd => spd.copy(droppable = true) + case opt @ Optional(t) => + Fld(FldFlags.empty.copy(opt = true), optionalType(term(t, Tpe), opt.toLoc), N) + // * Mirrors the elaboration of named fields in `fld`. + case field @ InfixApp(Optional(id: Ident), Keywrd(Keyword.`:`), rhs) => + Fld(FldFlags.empty.copy(opt = true), Term.Lit(StrLit(id.name).withLocOf(id)), S(optionalType(arg(rhs, Tpe), field.toLoc))) + case field => fld(field, Tpe) + // Parameter names label positions; they are not singleton string types. + // Normalize named fields here so resolution, erasure, and other consumers + // all read the parameter type from the same tuple element. + val elems = namedElems.map: + case f @ Fld(_, _, S(tpe)) => f.copy(term = tpe, asc = N) + case elem => elem + fields.lazyZip(elems).foldLeft(ParameterOrder.empty): + case (order, (field, Fld(flags, _, _))) => + order.checkFixed(flags.opt, field.toLoc) + order.next(flags.opt, false) + case (order, (field, _: Spd)) => + order.checkVariadic(field.toLoc) + order.next(false, true) + withTypeBinders(Term.Tup(elems)(tree)) + case _ => subterm(lhs, Tpe) + def mkNew(cls: Term, args: Ls[Term], rft: Opt[ClassSymbol -> ObjBody])(typ: Opt[typing.Type]): Term.New = val res = withTypeBinders(new Term.New(cls, args, rft)(FlowSymbol.neww(), typ).withLocOf(tree)) // The synthetic parent of an anonymous refinement (below) instantiates the same class at the same site, @@ -1459,12 +1551,14 @@ extends Importer: // `.class` chooses the class overload before requesting a receiver value. // Interpreting C as a receiver first would also select its companion. val preTrm = subterm(tree.prefix, if newResolution && tree.name.name == "class" then Clss else Receiver) - // `source` has no runtime object. Recognize its statically named fields + // Virtual modules have no runtime object. Recognize their literal members // before constructing a runtime selection, in either resolution mode. - // Compare symbols so a user-defined binding named `source` stays ordinary. + // Compare symbols so user-defined bindings named `source` or `unsafe` stay ordinary. val sym = if newResolution then preTrm.withoutCaptures match case Term.MemberRef(bms) if bms === ctx.builtins.source.bms => ctx.builtins.source.module.tree.definedSymbols.get(tree.name.name) + case Term.MemberRef(bms) if bms === ctx.builtins.unsafe.bms => + ctx.builtins.unsafe.module.tree.definedSymbols.get(tree.name.name) case _ => N else resolveField(tree.name, preTrm.symbol, tree.name) if sym.contains(ctx.builtins.source.line) then @@ -1476,6 +1570,8 @@ extends Importer: else if sym.contains(ctx.builtins.source.file) then val loc = tree.toLoc.getOrElse(???) Term.Lit(StrLit(loc.origin.fileName.toString)) + else if sym.contains(ctx.builtins.unsafe.undefined) then + Term.Lit(UnitLit(false).withLocOf(tree)) else if newResolution then val res = withTypeBinders(new Term.NewSel(preTrm, tree.name, N)(FlowSymbol.sel(tree.name.name)).withLocOf(tree)) rctx.recordAccessSite(res, ctx.scope) @@ -1535,7 +1631,7 @@ extends Importer: LetDecl(sym, Nil)(tree.toLoc) :: defineVar(sym, lt)(tree.toLoc) :: Nil, Term.Try(Blk( assignment(lt, subterm(rhs)) :: Nil, subterm(bod, interp), - ), assignment(lt, sym.ref()))) + ), assignment(lt, syntheticLocalRef(sym)))) case LetLike(Keywrd(Keyword.`set`), _, N, S(_)) => raise: ErrorReport(msg"Expected a right-hand side for this assignment" -> tree.toLoc :: Nil) @@ -1569,7 +1665,7 @@ extends Importer: case ParamList(_, value :: Nil, _) :: newParams => if newParams.isEmpty then raise(ErrorReport(msg"Handler function cannot be a getter" -> td.toLoc :: Nil)) - val newTd = TermDefinition(Fun, sym, tsym, newParams.reverse, tparams, sign, body, flags, mf, annotations, comp)(td.toLoc, td.headerLoc) + val newTd = TermDefinition(Fun, sym, tsym, newParams.reverse, tparams, sign, body, flags, mf, annotations, comp)(td.toLoc, td.headerLoc, td.scope) S(HandlerTermDefinition(value.sym, newTd)) case _ => raise(ErrorReport(msg"Handler function is missing resumption parameter" -> td.toLoc :: Nil)) @@ -1587,7 +1683,7 @@ extends Importer: (subterm(c, Clss), Nil) (ctx + (id.name -> sym)).givenIn: - Term.Handle(sym, cp, p, derivedClsSym, tds, subterm(bod, interp)) + Term.Handle(sym, cp, p, derivedClsSym, tds, subterm(bod, interp))(isAbortive = false) case h: Hndl => raise(ErrorReport( msg"Unsupported handle binding shape" -> @@ -1595,7 +1691,7 @@ extends Importer: error case kw @ Keywrd(Keyword.`this`) => (if newResolution then ctx.getReceiver else ctx.getOuter.map(Ctx.RefElem(_))) match - case S(elem) => elem.ref(new Ident("this").withLocOf(kw)) + case S(elem) => elem.ref(S(new Ident("this").withLocOf(kw))) case N => raise: ErrorReport(msg"Cannot use 'this' outside of an object scope" -> tree.toLoc :: Nil) @@ -1657,14 +1753,15 @@ extends Importer: QuantVar(sym, ub.map(ub => subterm(ub, Tpe)), lb.map(lb => subterm(lb, Tpe))) Term.Forall(bds, outer, subterm(body, Tpe)) case InfixApp(lhs, Keywrd(Keyword.`->`), Effectful(eff, rhs)) => - Term.FunTy(subterm(lhs, Tpe), subterm(rhs, Tpe), S(subterm(eff, Tpe))) + Term.FunTy(funTyParams(lhs), subterm(rhs, Tpe), S(subterm(eff, Tpe))) case InfixApp(lhs, Keywrd(Keyword.`->`), rhs) => - Term.FunTy(subterm(lhs, Tpe), subterm(rhs, Tpe), N) + Term.FunTy(funTyParams(lhs), subterm(rhs, Tpe), N) case InfixApp(lhs, Keywrd(Keyword.`=>`), rhs) => lhs match case Tup(_) => ctx.nest(OuterCtx.LambdaOrHandlerBlock).givenIn: val (syms, nestCtx) = funParams(lhs) + seedRestParameter(syms) withTypeBinders(Term.Lam(syms, term(rhs, Trm)(using nestCtx))) case TyTup(tys) => val constraints = tys.flatMap(maybeConstraint) @@ -1672,6 +1769,7 @@ extends Importer: Term.Constrained(constraints, body) case _ => lastWords(s"Unexpected lambda parameter shape: $lhs") case Keywrd(Keyword.`dyn`) => Term.DynTy().withLocOf(tree) + case Keywrd(Keyword.`undefined`) => Term.Lit(bareUndefined(tree)) case InfixApp(lhs, Keywrd(Keyword.`as`), rhs) => val body = subterm(lhs, interp) val sign = subterm(rhs, Tpe) @@ -1773,10 +1871,10 @@ extends Importer: case tree @ OpApp(lhs, op, rhss) => val sym = FlowSymbol.app() val (lhsDelimitsUnders, rhsDelimitsUnders) = underscoreDelimitingSides(op) - val lt = if lhsDelimitsUnders then delimitedTerm(lhs, Trm) else subterm(lhs) + val lt = if lhsDelimitsUnders then delimitedTerm(lhs, Trm, alwaysLambda = false) else subterm(lhs) val ot = subterm(op) val rts = rhss.map: r => - PlainFld(if rhsDelimitsUnders then delimitedTerm(r, Trm) else subterm(r)) + PlainFld(if rhsDelimitsUnders then delimitedTerm(r, Trm, alwaysLambda = false) else subterm(r)) app(ot, withTypeBinders(Term.Tup(PlainFld(lt) :: rts)(DummyTup)))( DummyApp, N, sym) case SynthSel(pre, nme) => @@ -1819,7 +1917,7 @@ extends Importer: case (S(owner: (ClassSymbol | ModuleOrObjectSymbol)), S(receiver)) => // Like other written selections, a by-name member selected through // `super` is instantiated at this site, within its type binders. - val res = withTypeBinders(new Term.Super(receiver.ref(new Ident("super").withLocOf(kw)), + val res = withTypeBinders(new Term.Super(receiver.ref(S(new Ident("super").withLocOf(kw))), owner, nme)(FlowSymbol.sel(nme.name)).withLocOf(sel)) if newResolution then rctx.recordAccessSite(res, ctx.scope) resolveSuper(res) @@ -1848,7 +1946,7 @@ extends Importer: N val self = VarSymbol(Ident("self"), erasedType = N) val args = VarSymbol(Ident("args"), erasedType = N) - val ps = ParamList(ParamListFlags.empty, + val ps = ParamList.withRest(ParamListFlags.empty, Param.simple(self) :: Nil, S: Param.simple(args) @@ -1856,9 +1954,9 @@ extends Importer: val rs = FlowSymbol.app() withTypeBinders(Term.Lam(ps, app( - if newResolution then elaborateProjection(self.ref(), c, nme) + if newResolution then elaborateProjection(syntheticLocalRef(self), c, nme) else Term.SelProj(self.ref(), c, nme)(f, FlowSymbol.selProj(nme.name), N, S(summon)), - args.ref())( + syntheticLocalRef(args))( App(nme, Tup(Nil)) // FIXME , N, rs) )) @@ -1899,8 +1997,10 @@ extends Importer: )(N) if mut then Term.Mut(inner) else inner case N => - val objectRef = ctx.builtins.Object.bms.get.ref(Ident("Object")) - mkNew(objectRef, Nil, bodo)(N) + val objectRef = Ctx.RefElem(ctx.builtins.Object.bms.get).ref(N) + // An anonymous object invokes Object's constructor, rather than + // partially applying its declared optional parameter list. + mkNew(objectRef, Term.Tup(Nil)(Tree.DummyTup) :: Nil, bodo)(N) // case _ => // raise(ErrorReport(msg"Illegal new expression." -> tree.toLoc :: Nil)) @@ -1984,7 +2084,7 @@ extends Importer: raise(ErrorReport(msg"Illegal outer binding." -> tree.toLoc :: Nil)) error case Outer(N) => ctx.get("outer") match - case S(sym) => interpretRef(sym.ref(Ident("outer")), interp) + case S(sym) => interpretRef(sym.ref(N).withLocOf(tree), interp) case N => raise(ErrorReport(msg"Illegal outer reference." -> tree.toLoc :: Nil)) error @@ -2011,7 +2111,8 @@ extends Importer: raise(ErrorReport(msg"Illegal position for '${kw.name}' modifier." -> kw.toLoc :: Nil)) subterm(body) case PrefixApp(Keywrd(Keyword.`=>`), body) if lang.newUnderscoreAndApplicationSyntax => - delimitedTerm(body, interp) + ctx.nest(OuterCtx.LambdaOrHandlerBlock).givenIn: + delimitedTerm(body, interp, alwaysLambda = true) case PrefixApp(kw, body) => raise(ErrorReport(msg"Illegal position for prefix keyword '${kw.name}'." -> kw.toLoc :: Nil)) subterm(body) @@ -2058,6 +2159,9 @@ extends Importer: case Spread(kw, body) => raise(ErrorReport(msg"Illegal position for '${kw.name}' spread operator." -> kw.toLoc :: Nil)) error + case Optional(body) => + raise(ErrorReport(msg"Illegal position for an optional marker '?': only parameters and function type parameters can be optional." -> tree.toLoc :: Nil)) + error case und: Under => summon[UnderCtx].unders match case N => @@ -2076,7 +2180,7 @@ extends Importer: val id = new Ident("_" + unds.size).withLocOf(und) val sym = VarSymbol(id, erasedType = N) unds += sym - sym.ref(id) + syntheticLocalRef(sym).withLocOf(und) case Annotated(lhs, rhs) => annot(lhs).fold(subterm(rhs, interp))(ann => annotated(ann, subterm(rhs, interp))) @@ -2104,9 +2208,9 @@ extends Importer: case InfixApp(lhs, Keywrd(Keyword.`:`), rhs) => Fld(FldFlags.empty, term(lhs, interp), S(arg(rhs, interp))) case Spread(Keywrd(Keyword.`..`), S(trm)) => - Spd(SpreadKind.Lazy, arg(trm, interp)) + Spd(SpreadKind.Lazy, arg(trm, interp), false) case Spread(Keywrd(Keyword.`...`), S(trm)) => - Spd(SpreadKind.Eager, arg(trm, interp)) + Spd(SpreadKind.Eager, arg(trm, interp), false) case _ => val t = arg(tree, interp) var flags = FldFlags.empty @@ -2149,6 +2253,18 @@ extends Importer: ): val members = blk.definedSymbols.toMap + // A qualified annotation can need a preceding val's receiver before the + // enclosing block finishes. Publish a member once all its overloads have + // definitions; waiting for unrelated later statements needlessly hides that + // receiver. The final completion pass still releases malformed definitions. + def completeIfReady(member: BlockMemberSymbol): Unit = + if member.trees.forall: + case td: Tree.TypeDef => td.symbol.defn.nonEmpty + case _: Tree.TermDef => member.asTrm.flatMap(_.defn).exists: defn => + // A separate signature is elaborated before the implementation; + // listeners must observe the final symbol and body together. + member.trmImplTree.isEmpty || defn.body.nonEmpty + then member.complete() val fieldInterpretation = resultInterp.elements val newSignatureTrees = mutable.Map.empty[Str, Tree] // * Store trees of signatures @@ -2311,7 +2427,7 @@ extends Importer: val sym = if newResolution then N else resolveField(id, baseElem.symbol, id) val e = Ctx.SelElem(baseElem, id.name, sym, isImport = true) // Validate the named selection even when its binding is unused. - if newResolution then e.ref(id) + if newResolution then e.ref(S(id)) id.name -> e :: Nil case t => raise(ErrorReport(msg"Illegal 'open' statement element." -> t.toLoc :: Nil)) @@ -2367,7 +2483,10 @@ extends Importer: 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. - recordFieldScope(RcdField.signature(Term.Lit(StrLit(id.name)).withLocOf(id), rhs_t)) :: acc + 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 case id: Ident => val sym = new VarSymbol(id, erasedType = N) newCtx += id.name -> sym @@ -2384,7 +2503,9 @@ extends Importer: ) :: acc case Bra(Round, inner) => reportUnusedAnnotations - recordFieldScope(RcdField(term(inner, Trm), rhs_t)) :: acc + val key = term(inner, Trm) + recordFieldScope(if fieldInterpretation == Tpe then RcdField.signature(key, rhs_t) + else RcdField(key, rhs_t)) :: acc case _ => raise(ErrorReport(msg"Unexpected record key shape." -> rlhs.toLoc :: Nil)) recordFieldScope(RcdField(Term.Error().withLocOf(rlhs), rhs_t)) :: acc @@ -2471,6 +2592,7 @@ extends Importer: val isMethod = owner.exists(_.isInstanceOf[ClassSymbol]) val tsym = TermSymbol(k, owner, id) // TODO? + val bindingScope = ctx.scope // Recorded before the definition's own type parameters enter scope. rctx.state.lexicalTypeBinders(tsym) = ctx.typeBinders @@ -2500,8 +2622,11 @@ extends Importer: case TypeDef(Mod, st, N) => term(st, Tpe)(using newCtx) case st => term(st, Tpe)(using newCtx) - if newResolution && td.annotatedResultType.isEmpty then - s.foreach(registerParameterSignature(pss, _)) + if td.annotatedResultType.isEmpty then + s.foreach: sign => + if newResolution then registerParameterSignature(pss, sign) + else checkSignatureOptionality(pss, sign) + pss.foreach(seedRestParameter) val body: Opt[Term] = rhs match case N => N @@ -2536,7 +2661,7 @@ extends Importer: if newResolution then s.foreach(registerSignature) val tdf = TermDefinition(k, sym, tsym, pss, tps, s, body, TermDefFlags.empty.copy(isMethod = isMethod, hasResultAnnotation = td.annotatedResultType.isDefined), mfn, annotations, N)( - statementLoc, definitionHeaderLoc(td, annotations, statementLoc)) + statementLoc, definitionHeaderLoc(td, annotations, statementLoc), S(bindingScope)) sym.tsym = S(tsym) tsym.defn = S(tdf) if newResolution then checkDeclaredResult(tdf) @@ -2546,6 +2671,7 @@ extends Importer: val tdf = debugElaboration(annotations): ctx.nest(OuterCtx.NonReturnContext(S(tsym))).givenIn(elaborateDefinition) + completeIfReady(sym) go(sts, Nil, tdf :: acc)(N) case L(d) => reportUnusedAnnotations @@ -2605,7 +2731,7 @@ extends Importer: case Modified(Keywrd(Keyword.`out`), id: Ident) => mk(id, S(true)) case _ => - raise(ErrorReport(msg"Unsupported type parameter ${targ.describe}" -> targ.toLoc :: Nil)) + raise(ErrorReport(msg"Unsupported type parameter shape: ${targ.describe}" -> targ.toLoc :: Nil)) Nil case N => Nil @@ -2620,13 +2746,9 @@ extends Importer: td.paramLists.map: ps => val (res, newCtx2) = given Ctx = newCtx - params(ps, isDataClass, k is Pat) + params(ps, isDataClass, k is Pat, inFunction = false, inConstructor = k is Cls) newCtx = newCtx2 - // Spread parameters are not supported in class parameters. - res.restParam.foreach: rp => - raise(ErrorReport( - msg"Spread parameters are not supported in class parameters." -> rp.toLoc :: Nil)) - res.copy(restParam = N)(res.toLoc) + withoutVariadic(res) def withFields(extraParams: Ls[ParamList])(using Ctx)(fn: (Ctx) ?=> (Term.Blk, Ctx)): (Term.Blk, Ctx) = softAssert(pss.sizeCompare(td.clsParams) === 0, @@ -2651,14 +2773,14 @@ extends Importer: tsym, Nil, N, N, if newResolution - then S(Term.Capture(Term.SimpleRef(p.sym)(Ident(p.sym.name)), tsym)) + then S(Term.Capture(Term.SimpleRef(p.sym)(N), tsym)) else S(p.sym.ref()) , TermDefFlags.empty, p.modulefulness, if isPublicField then Nil else Annot.Private :: Nil, N, - )(p.toLoc, p.toLoc) + )(p.toLoc, p.toLoc, S(ctx.scope)) assert(p.fldSym.isEmpty) p.fldSym = S(fsym) fsym.tsym = S(tsym) @@ -2674,7 +2796,7 @@ extends Importer: psym.sourceAliases = p.sym.sourceAliases rctx.state.lexicalTypeBinders(psym) = ctx.typeBinders val decl = LetDecl(psym, Nil)(p.toLoc) // TODO: never use term symbols on LetDecl LHS - val defn = defineVar(psym, p.sym.ref())(p.toLoc) + val defn = defineVar(psym, syntheticLocalRef(p.sym))(p.toLoc) p.fldSym = S(psym) decl :: defn :: Nil @@ -2758,9 +2880,10 @@ extends Importer: val allParams = ps.fold(Nil): _.params.flatMap: // Only `pat` flag is `true`. - case p @ Param(flags = FldFlags(false, false, true, false)) => S(p) + // (Optional and droppable parameters were already reported by `params`.) + case p @ Param(flags = FldFlags(false, false, true, false, _, _)) => S(p) // All flags are `false`. - case p @ Param(flags = FldFlags(false, false, false, false)) => S(p) + case p @ Param(flags = FldFlags(false, false, false, false, _, _)) => S(p) case Param(flags, sym, _, _) => raise(ErrorReport(msg"Unexpected pattern parameter ${sym.name} with modifiers: ${ flags.show(false)}" -> sym.toLoc :: Nil)) @@ -2842,12 +2965,9 @@ extends Importer: given Ctx = newCtx // Unlike main parameters, constructor(...) parameters of data classes are not // implicitly `val`: they only become fields when explicitly marked. - params(ps, false, false) + params(ps, false, false, inFunction = false, inConstructor = true) newCtx = newCtx2 - res.restParam.foreach: rp => - raise(ErrorReport( - msg"Spread parameters are not supported in class parameters." -> rp.toLoc :: Nil)) - res.copy(restParam = N)(res.toLoc) + withoutVariadic(res) newCtx.givenIn: trace(dbg"Processing class definition $nme"): val comp = sym.asMod @@ -2870,7 +2990,7 @@ extends Importer: case a @ Annot.Modifier(Keyword.`declare` | Keyword.`private` | Keyword.`public`) => a , S(clsSym), - )(statementLoc, headerLoc) + )(statementLoc, headerLoc, S(outerCtx.scope)) if pss.nonEmpty then sym.tsym = S(ctsym) ctsym.defn = S(ctdef) // Note: do NOT set sym.tsym for constructor(...) classes; they are not callable as functions. @@ -2884,6 +3004,7 @@ extends Importer: cd case Trt | Mxn => lastWords(s"Unexpected type definition kind here: $k") ) + completeIfReady(sym) go(sts, Nil, defn :: acc)(N) case Annotated(annotation, target) :: sts => go(target :: sts, annotations ++ annot(annotation), acc)(statementLoc) @@ -3019,28 +3140,84 @@ extends Importer: new Ident(base).withLocOf(id) -> (id.name :: Nil) case N => id -> Nil - val sig = sign.map(term(_, Tpe)) - val mfn = Modulefulness.ofSign(sig)(Mod in modifiers) + val writtenSig = sign.map(term(_, Tpe)) + val mfn = Modulefulness.ofSign(writtenSig)(Mod in modifiers) + // * The argument of an optional parameter may be omitted, in which case the parameter is `null`, + // * so the annotation of `y?: T` denotes `T | null` both for callers and in the body. + val sig = if flg.opt then writtenSig.map(optionalType(_, t.toLoc)) else writtenSig if newResolution then sig.foreach(registerSignature) val sym = VarSymbol(canonicalId) sym.sourceAliases = aliases val p = Param(flg, sym, sig, mfn)(t.toLoc) sym.decl = S(p) - if newResolution then sig.foreach: sign => - listenTypeInstances(sign): shape => - if sym.currentShapes.add(shape) then sym.notifyShapeListeners(shape) + if newResolution then + // Optional parameters admit null regardless of which calls we observe. + // Add null to unannotated parameters without giving them an annotation + // that would hide the shapes inferred from their arguments. + val initial = sig.orElse: + if flg.opt then S(Term.Lit(UnitLit(true).withLoc(t.toLoc))) else N + initial.foreach: sign => + listenTypeInstances(sign): shape => + if sym.currentShapes.add(shape) then sym.notifyShapeListeners(shape) (p, spd, aliases) + /** Variadic parameters are not supported in class parameters. Report them and keep their trailing parameters. */ + def withoutVariadic(ps: ParamList): ParamList = ps.variadic match + case S(v) => + raise(ErrorReport(msg"Spread parameters are not supported in class parameters." -> v.param.toLoc :: Nil)) + ps.copy(params = ps.params ::: v.trailing, variadic = N)(ps.toLoc) + case N => ps + + /** The type of an optional parameter or optional function type parameter annotated with `tpe`, + * where `loc` is the location of the optional parameter, which explains the `null` part. */ + def optionalType(tpe: Term, loc: Opt[Loc]): Term = + Term.CompType(tpe, Term.Lit(Tree.UnitLit(true).withLoc(loc)), true) + + /** A separate signature, as in `fun f: (Int, Int?) -> Int` followed by `fun f(x, y?) = ...`, + * determines how the function can be called, so it must agree with the implementation's + * parameter lists on which parameters are optional. */ + def checkSignatureOptionality(pss: Ls[ParamList], sign: Term): Unit = (pss, sign) match + case (_, Term.Forall(_, _, body)) => checkSignatureOptionality(pss, body) + case (ps :: pss, Term.FunTy(Term.Tup(fields), rhs, _)) => + ps.params.lazyZip(fields).foreach: + case (p, fld @ Fld(flags, _, _)) if p.flags.opt != flags.opt => + def describe(opt: Bool) = if opt then "optional" else "required" + raise(ErrorReport( + msg"Parameter '${p.sym.name}' is ${describe(p.flags.opt)}..." -> p.toLoc :: + msg"...but the corresponding parameter of the function's signature is ${describe(flags.opt)}." -> fld.toLoc :: Nil)) + case _ => () + checkSignatureOptionality(pss, rhs) + case _ => () + def funParams(t: Tree): Ctxl[(ParamList, Ctx)] = - val ps_ctx = params(t, inDataClass = false, inPattern = false) + val ps_ctx = params(t, inDataClass = false, inPattern = false, inFunction = true, inConstructor = false) def checkFlags(p: Param): Unit = if p.flags.isVal || p.flags.mut then raise(ErrorReport(msg"Illegal function parameter modifiers: ${ p.flags.show(false)}" -> p.sym.toLoc :: Nil)) ps_ctx._1.params.foreach(checkFlags) - ps_ctx._1.restParam.foreach(checkFlags) + ps_ctx._1.variadic.foreach(_.allParams.foreach(checkFlags)) ps_ctx + /** An unannotated rest parameter has Array's interface even before any call. + * Seed bottom elements as an inferred shape, not an annotation: argument + * tuples must still supply their element shapes and positional information. + * Call after registering separate signatures so those interfaces remain authoritative. + */ + private def seedRestParameter(ps: ParamList)(using Ctx): Unit = if newResolution then + ps.variadic.map(_.param).filter(parameterSignature(_).isEmpty).foreach: p => + val array = prelude.builtins.Array.ref(new Ident("Array").withLoc(p.toLoc)) + val nothing = prelude.builtins.Nothing.ref(new Ident("Nothing").withLoc(p.toLoc)) + val initial = Term.TyApp(array, nothing :: Nil)(N).withLoc(p.toLoc) + // Builtins ignore local name shadowing, but their interfaces still enter + // the parameter's lexical scopes. Without these captures a generic Array + // method mixes inference inside the function with inference outside it. + def capture(scope: Ctx): Term = + val outer = scope.parent.fold[Term](initial)(capture) + scope.outer.resolutionBoundary.fold(outer)(Term.Capture(outer, _)) + listenTypeInstances(capture(ctx)): shape => + if p.sym.currentShapes.add(shape) then p.sym.notifyShapeListeners(shape) + /** Elaborate a subtyping constraint. */ def constraint(lhs: Tree, op: "<:<" | ">:>", rhs: Tree): Ctxl[SubConstraint] = val l = term(lhs, Tpe) @@ -3064,12 +3241,19 @@ extends Importer: * @param inPattern Whether the parameter list belongs to a pattern definition. * If `inPattern` is `true`, only parameters with `pat` flag * will be added to the context. + * @param inFunction Whether the parameter list belongs to a function or lambda, + * whose parameters may be optional or droppable. + * @param inConstructor Whether the parameter list belongs to a class constructor. */ - def params(t: Tree, inDataClass: Bool, inPattern: Bool): Ctxl[(ParamList, Ctx)] = t match + def params(t: Tree, inDataClass: Bool, inPattern: Bool, inFunction: Bool, inConstructor: Bool): Ctxl[(ParamList, Ctx)] = t match case Tup(ps) => - def go(ps: Ls[Tree], acc: Ls[Param], ctx: Ctx, flags: ParamListFlags): (ParamList, Ctx) = + // * `acc` accumulates the leading parameters in reverse order, and `variadic` the variadic + // * parameter together with the trailing parameters following it, also in reverse order. + def go(ps: Ls[Tree], acc: Ls[Param], variadic: Opt[(Param, Ls[Param])], ctx: Ctx, flags: ParamListFlags, + order: ParameterOrder): (ParamList, Ctx) = ps match - case Nil => (ParamList(flags, acc.reverse, N)(t.toLoc), ctx) + case Nil => + (ParamList(flags, acc.reverse, variadic.map((p, trailing) => Variadic(p, trailing.reverse)))(t.toLoc), ctx) case hd :: tl => val isCtxParam = hd.isModified(Ins) val inUsing = flags.ctx || isCtxParam @@ -3077,21 +3261,38 @@ extends Importer: case R((p, spd, aliases)) => if isCtxParam && acc.nonEmpty then raise(ErrorReport(msg"Keyword `using` must occur before all parameters." -> hd.toLoc :: Nil)) + if p.flags.droppable && !inFunction then + raise(ErrorReport(msg"Droppable parameters are only supported in function and lambda parameter lists." -> hd.toLoc :: Nil)) + // * The arguments of a call are assigned to the parameters as follows: the required leading + // * parameters and the trailing parameters (which are all required) take the first and last + // * arguments, the optional leading parameters take the remaining ones from left to right, + // * and the variadic parameter takes the rest (see `ParamList` and `Lowering.setupFunctionDef`). + if p.flags.opt then + if !inFunction && !inConstructor then + raise(ErrorReport(msg"Optional parameters are only supported in function, lambda, and constructor parameter lists." -> hd.toLoc :: Nil)) + else if inUsing then + raise(ErrorReport(msg"Contextual parameters cannot be optional." -> hd.toLoc :: Nil)) + else if spd.isDefined then + raise(ErrorReport(msg"Variadic parameters cannot be optional." -> hd.toLoc :: Nil)) + else order.checkFixed(true, hd.toLoc) + else if spd.isEmpty then order.checkFixed(false, hd.toLoc) + spd.foreach(_ => order.checkVariadic(hd.toLoc)) + val nextOrder = order.next(p.flags.opt, spd.nonEmpty) val bindings = (p.sym.name -> p.sym) :: aliases.map(_ -> p.sym) val newCtx = if !inPattern || p.flags.pat then ctx ++ bindings else ctx val newFlags = flags.copy(ctx = inUsing) - spd match - case S(spd) => + (spd, variadic) match + case (S(spd), N) => if spd is SpreadKind.Lazy then raise(ErrorReport(msg"Lazy spread parameters not allowed." -> hd.toLoc :: Nil)) - if tl.isEmpty then - (ParamList(flags, acc.reverse, S(p))(t.toLoc), newCtx) - else - raise(ErrorReport(msg"Spread parameters must be the last in the parameter list." -> hd.toLoc :: Nil)) - go(tl, p :: acc, newCtx, newFlags) - case N => go(tl, p :: acc, newCtx, newFlags) - case L(d) => raise(d); go(tl, acc, ctx, flags) - go(ps, Nil, ctx, ParamListFlags.empty) + go(tl, acc, S(p -> Nil), newCtx, newFlags, nextOrder) + case (S(_), S((vp, trailing))) => + // * Keep the parameter as a trailing one so that its references remain bound. + go(tl, acc, S(vp -> (p :: trailing)), newCtx, newFlags, nextOrder) + case (N, S((vp, trailing))) => go(tl, acc, S(vp -> (p :: trailing)), newCtx, newFlags, nextOrder) + case (N, N) => go(tl, p :: acc, N, newCtx, newFlags, nextOrder) + case L(d) => raise(d); go(tl, acc, variadic, ctx, flags, order) + go(ps, Nil, N, ctx, ParamListFlags.empty, ParameterOrder.empty) case _ => raise: ErrorReport: @@ -3107,11 +3308,16 @@ extends Importer: // like explicit bindings, they take precedence over wildcard sources. state.builtinOpsMap.get(id.name) match case S(bi) => S(bi.ref(id)) - case N => candidate.map(_.ref(id)) - case S(elem) => S(elem.ref(id)) + case N => candidate.map(_.ref(S(id))) + case S(elem) => S(elem.ref(S(id))) def pattern(t: Tree): Ctxl[Pattern] = pattern(t, Map.empty) + private def bareUndefined(tree: Tree): Literal = + raise(WarningReport(msg"Direct use of 'undefined' is discouraged; use 'unsafe.undefined' for JavaScript interoperation." + -> tree.toLoc :: Nil)) + UnitLit(false).withLocOf(tree) + private def pattern(t: Tree, extractionBindings: Map[Str, VarSymbol]): Ctxl[Pattern] = import ucs.{Ctor, unapply, error}, ucs.extractors.*, Keyword.*, Pattern.*, InvalidReason.* /** String range bounds must be single characters. */ @@ -3136,10 +3342,10 @@ extends Importer: log("has elem!") elem.symbol.flatMap: case vs: VarSymbol => vs.decl match - case S(d) if d.isPatternConstructor => S(elem.ref(id)) + case S(d) if d.isPatternConstructor => S(elem.ref(S(id))) case S(_) | N => N case sym: Symbol => sym.asCls.orElse(sym.asObj).orElse(sym.asPat) match - case S(_) => S(elem.ref(id)) + case S(_) => S(elem.ref(S(id))) case N => N case N => state.builtinOpsMap.get(id.name) match @@ -3231,6 +3437,7 @@ extends Importer: case TyTup(ps) => tuple(ps) case Tup(ps) => tuple(ps) case t: syntax.Literal => Literal(t) + case Keywrd(Keyword.`undefined`) => Literal(bareUndefined(t)) // Negation patterns: `~p` case App(Ident("~"), Tup(p :: Nil)) => Negation(go(p)) // Negative integer and decimal literals. @@ -3306,7 +3513,11 @@ extends Importer: raise: ErrorReport(msg"Pattern name not found: ${id.name}." -> id.toLoc :: Nil) Pattern.Wildcard() - case sel: (SynthSel | Sel) => constructor(term(sel, Ptrn), N, sel) + case sel: (SynthSel | Sel) => term(sel, Ptrn) match + // Virtual-module selections such as `unsafe.undefined` are literal patterns, + // so negation and narrowing must see the literal rather than a constructor. + case Term.Lit(lit) => Literal(lit) + case target => constructor(target, N, sel) case _: Tree => raise(ErrorReport(msg"Unrecognized pattern (${t.describe})." -> t.toLoc :: Nil)) Pattern.Wildcard() @@ -3320,7 +3531,7 @@ extends Importer: sym.decl = S(TyParam(FldFlags.empty, N, sym)(id.toLoc)) Param.simple(sym) :: Nil case t => - raise(ErrorReport(msg"Unsupported type parameter ${t.describe}" -> t.toLoc :: Nil)) + raise(ErrorReport(msg"Unsupported type parameter shape: ${t.describe}" -> t.toLoc :: Nil)) Nil (vs, ctx ++ vs.map(p => p.sym.name -> p.sym)) case _ => @@ -3341,129 +3552,17 @@ extends Importer: def topLevel(sts: Block): Ctxl[(Blk, Ctx)] = given UnderCtx = UnderCtx.none val (res, ctx) = block(sts, hasResult = false, resultInterp = Trm) - computeVariances(res) if newResolution then if config.language.strictResolution then val exports = res.stats.collect: case d: Definition if InterfaceExposure.isPublic(d) => d.bsym val values = res.stats.collect: - case DefineVar(sym: LocalVarSymbol, rhs) => Term.SimpleRef(sym)(new Ident(sym.nme).withLocOf(rhs)) - InterfaceExposure(this).check(exports, res.res :: values) + case DefineVar(sym: LocalVarSymbol, rhs) => Term.SimpleRef(sym)(N) -> rhs.toLoc + InterfaceExposure(this).check(exports, (res.res -> res.res.toLoc) :: values) else // Explicit annotations still check callbacks whose signatures they hide. InterfaceExposure(this).check(Nil, Nil) rctx.completeBlock(res) (res, ctx) - def computeVariances(s: Statement): Unit = - val trav = VarianceTraverser() - def go(s: Statement): Unit = s match - case TermDefinition(k, sym, tsym, pss, _, sign, body, r, _, _, _) => - pss.foreach(ps => ps.params.foreach(trav.traverseType(S(false)))) - sign.foreach(trav.traverseType(S(true))) - body match - case S(b) => - go(b) - case N => - case ClassDef(sym, tps, pso, body) => - pso.foreach: ps => - ps.foreach: p => - p.sign.foreach(trav.traverseType(S(true))) - body.blk.stats.foreach(go) - // s.subStatements.foreach(go) - case _ => - s.subStatements.foreach(go) - while trav.changed do - trav.changed = false - go(s) - - class VarianceTraverser(var changed: Bool = true) extends Traverser: - override def traverseType(pol: Pol)(trm: Term): Unit = trm match - case Term.TyApp(lhs, targs) => - lhs.symbol.flatMap(sym => sym.asTpe) match - case S(sym: ClassSymbol) => - sym.defn match - case S(td: ClassDef) => - td.tparams.zip(targs).foreach: - case (tp, targ) => - if !tp.isContravariant then traverseType(pol)(targ) - if !tp.isCovariant then traverseType(pol.!)(targ) - case N => - // TODO(sym->sym.uid) - case S(sym: ModuleOrObjectSymbol) => - sym.defn match - case S(td: ModuleOrObjectDef) => - td.tparams.zip(targs).foreach: - case (tp, targ) => - if !tp.isContravariant then traverseType(pol)(targ) - if !tp.isCovariant then traverseType(pol.!)(targ) - case N => - // TODO(sym->sym.uid) - case S(sym: TypeAliasSymbol) => - // TODO dedup with above... - sym.defn match - case S(td: semantics.TypeDef) => - td.tparams.zip(targs).foreach: - case (tp, targ) => - if !tp.isContravariant then traverseType(pol)(targ) - if !tp.isCovariant then traverseType(pol.!)(targ) - case N => - TODO(sym->sym.uid) - // case S(sym) => ??? - case N => - log(dbg"No symbol found $lhs ${lhs.symbol}") - // ??? - () // TODO - case Term.Ref(sym: VarSymbol) => - sym.decl match - case S(ty: TyParam) => - if pol =/= S(true) && ty.isCovariant then - changed = true - ty.isCovariant = false - if pol =/= S(false) && ty.isContravariant then - changed = true - ty.isContravariant = false - case S(decl) => - lastWords(s"VarSymbol ${sym.name} has unexpected declaration: $decl") - case N => - lastWords(s"VarSymbol ${sym.name} has no declaration") - case _ => super.traverseType(pol)(trm) - abstract class Traverser: - def traverseType(pol: Pol)(trm: Term): Unit = trm match - case Term.Lit(_) | Term.UnitVal() | Term.Error => - case Term.TyApp(lhs, targs) => - // lhs.resolveSymbol - // targs.foreach(traverseType(pol)) - ??? - case r: Term.Ref => - case Term.FunTy(l, r, e) => - traverseType(pol.!)(l) - traverseType(pol)(r) - e.foreach(e => traverseType(pol)(e)) - case Term.Forall(_, _, body) => - traverseType(pol)(body) - case Term.WildcardTy(in, out) => - in.foreach(t => traverseType(pol.!)(t)) - out.foreach(t => traverseType(pol)(t)) - case Term.CompType(lhs, rhs, _) => () // TODO: - case Term.SynthSel(bse, nme) => - traverseType(pol)(bse) // FIXME: probably wrong for what we want to do - case Term.Tup(fields) => - // fields.foreach(f => traverseType(pol)(f.value)) - fields.foreach(traverseType(pol)) - // case _ => ??? - case Term.Neg(ty) => - traverseType(pol.!)(ty) - case _ => - // TODO - def traverseType(pol: Pol)(f: Elem): Unit = f match - case f: Fld => - traverseType(pol)(f.term) - f.asc.foreach(traverseType(pol)) - case _: Spd => TODO("variance traversal of spread elements") - def traverseType(pol: Pol)(f: Param): Unit = - f.sign.foreach(traverseType(pol)) end Elaborator - -type Pol = Opt[Bool] -extension (p: Pol) def ! : Pol = p.map(!_) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala index f2ded6d33d..270d38ed34 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Importer.scala @@ -66,7 +66,7 @@ class Importer extends NewResolver: // binding. Publish its shapes so selection and opens can follow it. if newResolution then sym match case local: VarSymbol => - val ref = Term.MemberRef(importedSym)(local.id, FlowSymbol.memSym(importedSym)) + val ref = Term.MemberRef(importedSym)(N, FlowSymbol.memSym(importedSym)) rctx.recordLexicalBinders(ref, ctx.typeBinders) pipeTerm(ref, local) case _: BlockMemberSymbol => () diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/InterfaceExposure.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/InterfaceExposure.scala index 24a9686a90..82a1745d4b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/InterfaceExposure.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/InterfaceExposure.scala @@ -55,7 +55,7 @@ final class InterfaceExposure(resolver: NewResolver)(using ResolutionContext, TL private def source(symbol: BlockMemberSymbol): MemberRef = val flow = flows.getOrElseUpdate(symbol, FlowSymbol("exposed")) - val ref: MemberRef = MemberRef(symbol)(new syntax.Tree.Ident(symbol.nme).withLocOf(symbol), flow) + val ref: MemberRef = MemberRef(symbol)(N, flow) // An exported definition is invoked from outside the unit, where no binder is in scope. rctx.recordLexicalBinders(ref, Set.empty) ref @@ -83,7 +83,7 @@ final class InterfaceExposure(resolver: NewResolver)(using ResolutionContext, TL case NoShape => ()))(emit(_, path)) private def parameter(param: Param, marks: Ls[Marks], path: Path)(using ResolutionContext): Unit = - val ref = SimpleRef(param.sym)(param.sym.id) + val ref = SimpleRef(param.sym)(N) val unknown = UnknownValueShape(ref, fromPublicInterface = true)(path.via( msg"Parameter '${param.sym.nme}' admits values of unknown shape." -> param.toLoc)) resolver.parameterSignature(param) match @@ -95,8 +95,9 @@ final class InterfaceExposure(resolver: NewResolver)(using ResolutionContext, TL private def parameters(lists: Ls[(ParamList, Ls[Marks])], path: Path)(using ResolutionContext): Unit = lists.foreach: (params, marks) => params.params.foreach(parameter(_, marks, path)) - params.restParam.foreach: rest => - val ref = SimpleRef(rest.sym)(rest.sym.id) + params.trailingParams.foreach(parameter(_, marks, path)) + params.variadic.map(_.param).foreach: rest => + val ref = SimpleRef(rest.sym)(N) val unknown = UnknownValueShape(ref, fromPublicInterface = true)(path.via( msg"Rest parameter '${rest.sym.nme}' admits elements of unknown shape." -> rest.toLoc)) val tuple = TupleShape(ref, TupleShape.Unknown(ref, TupleShape.ValueField(unknown, Nil)) :: Nil)(resolver) @@ -114,12 +115,9 @@ final class InterfaceExposure(resolver: NewResolver)(using ResolutionContext, TL watch((new Identity(body), marks, "result"))(resolver.listenResult(body)): shape => emit(shape.exit(marks), path.via(msg"This value is returned here." -> body.toLoc)) case S(sign) => - // Check returned implementations against the declared calling interface. - // Its parameter types constrain returned closures instead of unrestricted - // unknowns, and consumers see only the annotated result. + // checkDeclaredResult already checks the body during elaboration. + // Exposure follows the declared result's interface. val declared = resolver.declaredType(resolver.typeResolution(sign), Map.empty).instantiate(rctx.instances) - watch((new Identity(body), declared, marks))(resolver.listenResult(body)): shape => - resolver.constrainFunction(declared, shape, marks) watch((declared, marks))(resolver.listenTypeInstances(declared)): shape => emit(shape.exit(marks), path) @@ -184,7 +182,7 @@ final class InterfaceExposure(resolver: NewResolver)(using ResolutionContext, TL // still admit unknown shapes even when local callers supplied evidence. val unknown = UnknownValueShape(callable.source, fromPublicInterface = true)(path) callable.paramLists.foreach: params => - (params.params.flatten ::: params.rest.toList).foreach: input => + params.allParams.flatMap(_.tpe).foreach: input => unknown.enter(marks) match case value: TermShape => resolver.exposeTypeHoles(input, value, marks) case NoShape => () @@ -200,16 +198,19 @@ final class InterfaceExposure(resolver: NewResolver)(using ResolutionContext, TL resolver.arrayElementType(nominal).foreach: binding => watch((ShapeIdentity.key(shape), "array elements"))(listener => { resolver.listenArrayElements(shape)(listener); () }): element => emit(element, path.via(msg"This value is stored in this array." -> binding.resolution.source.toLoc)) + case view: IntersectionView => view.values.foreach(value => emit(value.exit(marks), path)) // Structural annotations hide initializer implementations. Unknown and // dynamic values have no static graph. case _: (RecordTypeShape | OpaqueTypeShape | UnknownValueShape | RigidTypeShape | DynShape | ErrShape) => () - def check(exports: Ls[BlockMemberSymbol], values: Ls[Term]): Unit = + /** An exposed value can be a synthesized local reference. Keep the exposure + * site's location in its diagnostic provenance, not on that reference. */ + def check(exports: Ls[BlockMemberSymbol], values: Ls[(Term, Opt[Loc])]): Unit = try exports.foreach: symbol => member(symbol, Nil, ShapeProvenance.empty.via(msg"'${symbol.nme}' is exposed by this compilation unit." -> symbol.headerLoc)) - values.foreach: value => - term(value, Nil, ShapeProvenance.empty.via(msg"This value is exposed by this compilation unit." -> value.toLoc)) + values.foreach: (value, loc) => + term(value, Nil, ShapeProvenance.empty.via(msg"This value is exposed by this compilation unit." -> loc)) // Annotated callbacks can escape even from private code. Delay their // exposure until the block is elaborated, so forward definitions and // returned closures are available. Processing one root may discover more. diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/MemberKey.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/MemberKey.scala new file mode 100644 index 0000000000..eb125b2114 --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/MemberKey.scala @@ -0,0 +1,40 @@ +package hkmc2 +package semantics + +import hkmc2.utils.*, shorthands.* + +/** Named source members and fresh values occupy distinct key spaces. + * A jsSymbol annotation gives a method a source name for its computed property. + */ +type MemberKey = Str | SymbolKey + +/** The identity includes the receiver: two instances' fresh fields need not hold + * the same value. The source expression is retained for diagnostics, but locations + * and capture wrappers do not participate in member-key equality. + */ +final case class SymbolKey(path: Ls[Symbol])(val term: Term): + def name: Str = path.map(_.nme).mkString(".") + override def toString: Str = name + +object MemberKey: + def index(key: MemberKey): Opt[Int] = key match + case name: Str => name.toIntOption + case _: SymbolKey => N + + def symbol(annotations: Ls[Annot]): Opt[SymbolKey] = annotations.collectFirst: + case Annot.JsSymbol(key) => key + + // Unsupported annotation targets are diagnosed as unused by lowering. They + // must keep their string property in both lookup and code generation. + def symbol(definition: TermDefinition): Opt[SymbolKey] = + if !definition.tsym.isPrivate && (definition.k == syntax.RecordField || + definition.k == syntax.Fun && definition.flags.isMethod) + then symbol(definition.annotations) else N + + def symbol(member: BlockMemberSymbol): Opt[SymbolKey] = + member.asTrm.flatMap(_.defn).flatMap(td => symbol(td)) + + def own(defn: ClassLikeDef, key: MemberKey): Opt[BlockMemberSymbol] = key match + case name: Str => defn.body.members.get(name) + case key: SymbolKey => + defn.body.methods.find(method => symbol(method).contains(key)).map(_.bsym) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala index f99569380f..2e36fc1fc3 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolver.scala @@ -13,6 +13,14 @@ import Elaborator.* import hkmc2.syntax.LetBind +/** Best-effort name, member, and call resolution, not a type checker. + * Shape propagation helps find targets and diagnose obvious mistakes; it does + * not prove type safety, even in strict mode. Leave complicated or expensive + * compatibility checks to a later type-checking pass and accept those cases + * leniently. This does not permit dropping flows: selections and ambiguity + * detection need every possible target. Approximate complicated relationships + * by joining their flows, even when lengths or correlations are lost. + */ class NewResolver: self: Elaborator => import tl.* @@ -133,28 +141,32 @@ class NewResolver: result.publish(TypeShape.Parameter(sym, sym.inferenceHost)) case Ref(sym: VarSymbol) if sym.decl.exists(_.isInstanceOf[TyParam]) => result.publish(TypeShape.Parameter(sym, sym.inferenceHost)) - case Tup(fields) if fields.forall(_.isInstanceOf[Fld]) => - result.publish(TypeShape.Tuple(fields.collect { case Fld(_, sign, _) => typeResolution(sign) })) + case Tup(fields) => result.publish: + TypeShape.Tuple: + fields.map: + case Fld(_, sign, _) => TypeShape.TupleField(typeResolution(sign), spread = false) + case Spd(_, sign, _) => TypeShape.TupleField(typeResolution(sign), spread = true) case record: Rcd => val fields = record.stats.collect { case field: RcdField => field } - if fields.length != record.stats.length || fields.exists(field => field.field match - case Lit(_: Tree.StrLit) => false - case _ => true) + if fields.length != record.stats.length || fields.exists(_.key.isEmpty) then result.fail(msg"A record type requires statically named fields." -> record.toLoc :: Nil) result.publish(TypeShape.Abstract) else result.publish(TypeShape.Record(record, - fields.reverse.distinctBy(_.sym.nme).reverse.map(field => field -> typeResolution(field.rhs)))) + fields.reverse.distinctBy(_.key).reverse.map(field => field -> typeResolution(field.rhs)))) // Signed numeric literals use operator applications in the syntax tree. // Their primitive interface is not recovered from these applications yet. case App(Ref(op: BuiltinSymbol), Tup(Fld(_, Lit(_: Tree.IntLit | _: Tree.DecLit), _) :: Nil)) if op.nme === "-" || op.nme === "+" => result.publish(TypeShape.Abstract) + // Ignore undefined in annotations: `T | undefined` has the same + // resolution candidates as T. + case Lit(Tree.UnitLit(false)) => result.publish(TypeShape.Bottom) case literal: Lit => // Singleton annotations retain literal values, which read no binders. rctx.recordTemplateBinders(result, S(Set.empty)) listenTerm(literal)(shape => result.publish(TypeShape.Inferred(shape))) - case _: Tup | Missing | Error() => + case Missing | Error() => result.publish(TypeShape.Abstract) case _ => listen(term, discardMarks = true): case shape: SymShape => shape.sym.onComplete: () => @@ -329,7 +341,8 @@ class NewResolver: case S(TypeShape.Nominal(_)) => args.foreach(guardedChild) // An abstract constructor could forward an argument unchanged. case _ => args.foreach(child) - case TypeShape.Tuple(fields) => fields.foreach(guardedChild) + case TypeShape.Tuple(fields) => fields.foreach: field => + if field.spread then child(field.tpe) else guardedChild(field.tpe) case TypeShape.Record(_, fields) => fields.foreach((_, field) => guardedChild(field)) case TypeShape.Function(params, result) => guardedChild(typeResolution(params)); guardedChild(result) case TypeShape.Polymorphic(params, _, body) => @@ -400,7 +413,7 @@ class NewResolver: applications += ((res, symbol, rhs, args)) visit(base) args.foreach(visit) - case TypeShape.Tuple(fields) => fields.foreach(visit) + case TypeShape.Tuple(fields) => fields.foreach(field => visit(field.tpe)) case TypeShape.Record(_, fields) => fields.foreach((_, field) => visit(field)) case TypeShape.Function(params, result) => visit(typeResolution(params)); visit(result) case TypeShape.Polymorphic(params, _, body) => @@ -451,6 +464,9 @@ class NewResolver: case S(TypeShape.Nominal(_)) => () case _ => args.foreach(follow) case TypeShape.Union(left, right) => follow(left); follow(right) + // A spread splices its operand into this tuple; it does not place + // that operand under a tuple field that could guard recursion. + case TypeShape.Tuple(fields) => fields.filter(_.spread).foreach(field => follow(field.tpe)) case TypeShape.Intersection(left, right) => follow(left); follow(right) case TypeShape.Negation(base) => follow(base) case TypeShape.Wildcard(input, output) => input.foreach(follow); output.foreach(follow) @@ -493,9 +509,8 @@ class NewResolver: // Constructor analysis ignores scope crossings, but still follows // the alias's substitution. The guard bounds body expansion; // substitution values are finite immutable reference DAGs. - val substitutions = symbol.defn.get.tparams.zip(args).map: (formal, argument) => - formal.sym -> argumentType(declaredArgument(argument, tpe, true), formal) - uses(declaredType(rhs, tpe.bindings ++ substitutions, tpe.positive), wrapped, next, aliases + symbol) + val substitutions = aliasBindings(tpe, typeApplication(args, tpe), symbol.defn.get.tparams, tpe.positive) + uses(declaredType(rhs, substitutions, tpe.positive), wrapped, next, aliases + symbol) case S(_) => dependencies(tpe.resolution).flatMap(bound(_, wrapped)) case N => dependencies(tpe.resolution).flatMap(bound(_, true)) case _ => dependencies(tpe.resolution).flatMap(bound(_, true)) @@ -594,7 +609,7 @@ class NewResolver: // Normalize both parts before retaining the argument. Otherwise even // out A keeps another A-to-wildcard environment at each recursive // step. Preserve the written source: it overrides declaration variance. - val parts = TypeArgument(input.fold(extremeType(false, S(resolution)))(loop(_, bindings, aliases, !positive)), + val parts = TypeArgument(input.fold(extremeType(false))(loop(_, bindings, aliases, !positive)), output.fold(missingOutput(resolution))(loop(_, bindings, aliases, positive))) normalizedArgument(resolution, parts) case TypeShape.Applied(base, arguments) :: Nil => @@ -607,7 +622,7 @@ class NewResolver: // Omitted formals use the same source holes as deferred // interpretation. Retaining a partial forwarding application // would nest its environment at each recursive projection. - S(loop(rhs, typeBindings(base, supplied, parameters, true), aliases + symbol, positive)) + S(loop(rhs, aliasBindings(base, supplied, parameters, positive), aliases + symbol, positive)) case TypeShape.Contextual(reference) :: Nil => // Use exactly the interpreter's application rule: move caller // arguments to the template endpoint, substitute there, then @@ -628,8 +643,8 @@ class NewResolver: else loop(resolution, bindings, Set.empty, positive) /** Preserve both occurrences of an unqualified argument before substitution - * can select either bound. A nominal application composes these with its own - * polarity; an alias binds them before interpreting its body at that polarity. + * can select either bound. Applications compose these with their polarity; + * aliasBindings also accounts for the alias body's local substitution scope. */ private def declaredArgument(resolution: TypeResolution, context: DeclaredType, positive: Bool) (using ResolutionContext): DeclaredType = @@ -712,29 +727,28 @@ class NewResolver: case _ => argument private def instanceType(instance: TypeParameterInstance)(using ResolutionContext): DeclaredType = - declaredType(typeResolution(instance.reference), Map.empty) + parameterType(TypeShape.Parameter(instance, instance.inferenceHost)) private def parameterType(parameter: TypeShape.Parameter)(using ResolutionContext): DeclaredType = rctx.state.parameterTypes.getOrElseUpdate(parameter, { - val source = SimpleRef(parameter.symbol)(parameter.symbol.id) + // This endpoint is shared across occurrences. Written uses and declaration + // notes carry their own locations; the canonical parameter has no source use. + val source = SimpleRef(parameter.symbol)(N) val resolution = new TypeResolution(source, messages => resolError(source, messages)) resolution.publish(parameter) declaredType(resolution, Map.empty) }) private def extremeType(top: Bool)(using ResolutionContext): DeclaredType = - extremeType(top, N) - - private def extremeType(top: Bool, source: Opt[TypeResolution])(using ResolutionContext): DeclaredType = - rctx.state.extremeTypes.getOrElseUpdate((top, source), { - val resolution = new TypeResolution(source.fold[Term](Term.Missing)(_.source), + rctx.state.extremeTypes.getOrElseUpdate(top, { + val resolution = new TypeResolution(Term.Missing, _ => lastWords("A synthesized extreme type cannot be invalid")) resolution.publish(if top then TypeShape.Top else TypeShape.Bottom) declaredType(resolution, Map.empty) }) private def missingOutput(source: TypeResolution)(using ResolutionContext): DeclaredType = - extremeType(true, S(source)).withOrigin(S(TypeInterfaceReason.MissingOutput(source.source))) + extremeType(true).withOrigin(S(TypeInterfaceReason.MissingOutput(source.source))) /** Written wildcards override declaration variance. Unqualified arguments have * already been interpreted in both polarities; declaration variance keeps the @@ -814,11 +828,11 @@ class NewResolver: declaredType(resolution, Map.empty) }) - /** Save both interpretations before traversing the template reference. A - * nominal template uses the application's polarity, whereas an alias binds - * the positive pair and composes polarity while interpreting its body. This - * keeps Identity[A] transparent and preserves captured enclosing binders. - * Each interpretation is itself an input/output argument pair. + /** Save both interpretations before traversing the template reference. Each + * interpretation is itself an input/output argument pair, interpreted in the + * caller's scope. Applications choose their polarity before applying variance; + * aliasBindings reorients new formals for lookup inside the alias body without + * changing the bindings of captured enclosing parameters. * * Supplied arguments belong to the application scope and move back to the * template's endpoint before substitution. Omitted arguments instead belong @@ -851,6 +865,20 @@ class NewResolver: effectiveBindings(source) ++ parameters.zip(typeArguments(source, arguments, parameters, positive)).map: (parameter, argument) => parameter.sym -> argumentType(argument, parameter) + /** Interpret arguments in the caller's polarity, but select their bounds + * relative to the alias body. The body retains the caller's polarity for its + * captured binders; reverse the new formal bindings under a negative caller + * so the enclosing domain does not also reverse which bound they select. + * Thus `Source[in Nothing out A]` still reads A from `{item: T}` under a + * domain. Both declaration variance and written bounds use this same rule. + */ + private def aliasBindings(source: DeclaredType, arguments: TypeApplication, parameters: Ls[TyParam], positive: Bool) + (using ResolutionContext): Map[VarSymbol, DeclaredType] = + effectiveBindings(source) ++ parameters.zip(typeArguments(source, arguments, parameters, positive)).map: (parameter, argument) => + val bound = argumentType(argument, parameter) + parameter.sym -> (if positive then bound else normalizedArgument(bound.resolution, + TypeArgument(selectArgument(bound, true), selectArgument(bound, false)))) + /** External inputs contribute unknowns only to missing annotation parts. * A written binder still denotes its declared type, including after a partial * application fixes it; exposure must not add arbitrary values to that binder. @@ -878,12 +906,12 @@ class NewResolver: case TypeShape.Applied(base, arguments) => follow(declaredType(base, current), typeApplication(arguments, current), captures, next) case TypeShape.Alias(symbol, S(rhs)) => - follow(declaredType(rhs, typeBindings(current, args, symbol.defn.get.tparams, true), current.positive), noTypeArguments, captures, next) + follow(declaredType(rhs, aliasBindings(current, args, symbol.defn.get.tparams, current.positive), current.positive), noTypeArguments, captures, next) case TypeShape.Nominal(cls) => cls.tparams.zip(typeArguments(current, args, cls.tparams, current.positive)).foreach: (param, argument) => listenArgumentParts(argumentType(argument, param)): parts => exposeTypeHoles(transportType(parts.output, captures), unknown, marks) - case TypeShape.Tuple(fields) => fields.foreach(field => descend(declaredType(field, current))) + case TypeShape.Tuple(fields) => fields.foreach(field => descend(declaredType(field.tpe, current))) case TypeShape.Record(_, fields) => fields.foreach((_, field) => descend(declaredType(field, current))) case TypeShape.Function(_, result) => descend(declaredType(result, current)) case TypeShape.Polymorphic(parameters, _, body) => @@ -943,6 +971,11 @@ class NewResolver: listenInstanceViews(value, mergeIntersections = false)(listener) private def listenInstanceViews(value: TermShape, mergeIntersections: Bool)(listener: Listener)(using ResolutionContext): Unit = value match + case Marked(view: IntersectionView, marks) if !mergeIntersections => + view.values.foreach: value => + transportShape(value, marks :: Nil) match + case value: TermShape => listenInstanceViews(value, mergeIntersections)(listener) + case NoShape => () case Marked(instance: InstanceShape, marks) => listenTypeViews(transportType(instance.tpe, marks :: Nil), mergeIntersections)(listener) case _ => listener(value) @@ -992,7 +1025,9 @@ 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.source, record.declarations) + RecordTypeShape(record.fields.map((name, tpe) => name -> instantiateType(tpe, projected)), record.aliases)(record.source, record.declarations) + case view: IntersectionView => + IntersectionView(view.values.map(instantiateShape(_, instances))) case specialized: SpecializedShape => val projected = project(instances, shapeDependencies(specialized), specialized.instances) if projected.isEmpty then specialized else rctx.shapeView(projected): @@ -1007,7 +1042,7 @@ class NewResolver: TypeScheme(scheme.owner, scheme.parameters.map: parameter => DeclaredTypeParameter(parameter.parameter, parameter.lower.map(instantiate), parameter.upper.map(instantiate))) CallableTypeShape(callable.source, callable.paramLists.ne_map: params => - DeclaredParams(params.params.map(_.map(instantiate)), params.hasRest, params.rest.map(instantiate)) + params.mapTypes(instantiate) , callable.result.map(instantiate), scheme, callable.supplied, callable.declaration, callable.asyncResult) case literal: IntroShape if !literal.trm.isInstanceOf[Lam] => literal case _: (UnknownValueShape | DynShape | OpaqueTypeShape | ErrShape) => source @@ -1156,8 +1191,10 @@ class NewResolver: 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))) + case view: IntersectionView => + view.values.foldLeft[Binders](S(Set.empty))((acc, value) => union(acc, shapeSupport(value))) case callable: CallableTypeShape => - val types = callable.paramLists.flatMap(params => params.params.flatten ::: params.rest.toList) ::: + val types = callable.paramLists.flatMap(params => params.allParams.flatMap(_.tpe)) ::: callable.result.toList ::: callable.supplied.toList.flatten ::: callable.scheme.toList.flatMap(_.parameters.flatMap(parameter => parameter.lower.toList ::: parameter.upper.toList)) types.foldLeft[Binders](S(Set.empty))((acc, tpe) => union(acc, typeSupport(tpe))) @@ -1272,19 +1309,30 @@ class NewResolver: * field combinations; a separate cache keyed by whole record sets is unnecessary. */ private def intersectViews(values: Ls[TermShape])(publish: Listener)(using ResolutionContext): Unit = - val records = values.collect: + val flattened = values.flatMap: + case Marked(view: IntersectionView, marks) => view.values.flatMap(value => transportShape(value, marks :: Nil) match + case value: TermShape => S(value) + case NoShape => N) + 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.source, record.declarations) - records.headOption.foreach: first => + RecordTypeShape(record.fields.map((name, tpe) => name -> transportType(tpe, marks :: Nil)), 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))) - RecordTypeShape(fields)(left.source, right.declarations ++ left.declarations) - publish(merged) - values.distinctBy(ShapeIdentity.key).foreach: - case Marked(_: RecordTypeShape, _) => () - case other => publish(other) + 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) + merged + val components = merged.toList ::: flattened.filter: + case Marked(_: RecordTypeShape, _) => false + case _ => true + components.distinctBy(ShapeIdentity.key) match + case value :: Nil => publish(value) + case Nil => () + case values => publish(IntersectionView(values)) private def listenTypeViews(tpe: DeclaredType)(listener: Listener)(using ResolutionContext): Unit = listenTypeViews(tpe, mergeIntersections = true)(listener) @@ -1372,7 +1420,7 @@ class NewResolver: // references, not symbols: Id[Id[T]] has two distinct references. if aliases(current.resolution) then publish(OpaqueTypeShape(source)(TypeInterfaceReason.RecursiveAlias(symbol))) else rhs match - case S(rhs) => follow(declaredType(rhs, bind(symbol.defn.get.tparams, true), current.positive), noTypeArguments, + case S(rhs) => follow(declaredType(rhs, aliasBindings(current, args, symbol.defn.get.tparams, current.positive), current.positive), noTypeArguments, aliases + current.resolution, source, publish) case N => publish(OpaqueTypeShape(source)(TypeInterfaceReason.AbstractDeclaration(symbol, source))) case TypeShape.Applied(base, params) => @@ -1406,10 +1454,21 @@ class NewResolver: case NoShape => ()) case TypeShape.Function(params, ret) => val ps = params match - case Tup(fields) => DeclaredParams(fields.collect: - case Fld(_, sign, _) => S(declaredType(typeResolution(sign), current.withPolarity(!current.positive))) - , fields.exists(!_.isInstanceOf[Fld]), N) - case single => DeclaredParams(S(declaredType(typeResolution(single), current.withPolarity(!current.positive))) :: Nil, false, N) + case Tup(fields) => + def parameter(fld: Fld) = + DeclaredParam(S(declaredType(typeResolution(fld.term), current.withPolarity(!current.positive))), fld.flags) + // Elaboration reports function types with several variadic parameters. + val (leading, variadic) = fields.span(_.isInstanceOf[Fld]) + val leadingFlds = leading.collect { case fld: Fld => fld } + DeclaredParams(leadingFlds.map(parameter), variadic match + case (spd: Spd) :: trailing => + val trailingFlds = trailing.collect { case fld: Fld => fld } + S(Variadic(DeclaredParam(S(declaredType(typeResolution(spd.term), current.withPolarity(!current.positive))), + FldFlags.empty.copy(droppable = spd.droppable)), trailingFlds.map(parameter))) + case _ => N) + case single => + DeclaredParams(DeclaredParam(S(declaredType(typeResolution(single), current.withPolarity(!current.positive))), + FldFlags.empty) :: Nil, N) publish(CallableTypeShape(source, ps ne_:: Nil, S(declaredType(ret, current)), N, N, N, N)) case TypeShape.Polymorphic(params, _, body) => @@ -1426,11 +1485,32 @@ class NewResolver: case NoShape => () case _ => publish(shape)) case TypeShape.Tuple(fields) => - publish(TupleShape(current.resolution.source, - fields.map(field => TupleShape.TypedField(declaredType(field, current), Nil)))(this)) + def expand(rest: Ls[TypeShape.TupleField], reversed: Ls[TupleShape.Element])(using ResolutionContext): Unit = rest match + case Nil => publish(TupleShape(current.resolution.source, reversed.reverse)(this)) + case TypeShape.TupleField(field, false) :: tail => + expand(tail, TupleShape.TypedField(declaredType(field, current), Nil) :: reversed) + case TypeShape.TupleField(field, true) :: tail => + listenTypeViews(declaredType(field, current), mergeIntersections = false): + case Marked(tuple: TupleShape, marks) => + expand(tail, TupleShape.Spread(tuple, marks) :: reversed) + case Marked(array: NominalInstanceView, marks) if arrayElementType(array).nonEmpty => + // Retain the symbolic element endpoint: sampling its + // current values would lose inference from later calls. + val element = TupleShape.TypedField(arrayElementType(array).get, marks :: Nil) + expand(tail, TupleShape.Unknown(field.source, element) :: reversed) + case shape @ Marked(_: DynShape | _: UnknownValueShape | _: OpaqueTypeShape, _) => + expand(tail, TupleShape.Unknown(field.source, TupleShape.ValueField(shape, Nil)) :: reversed) + case shape => + field.fail(msg"A tuple type spread requires an Array or tuple interface." -> field.source.toLoc :: + shape.diagnostic(msg"This type does not supply an array interface.")) + expand(fields, Nil) case TypeShape.Record(source, fields) => - publish(RecordTypeShape(fields.map((field, sign) => field.sym.nme -> declaredType(sign, current)).toMap)( - source, fields.map((field, _) => field.sym.nme -> field).toMap)) + val aliases = fields.flatMap: + 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)( + 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) case TypeShape.Intersection(left, right) => @@ -1455,20 +1535,60 @@ class NewResolver: // publish inferred alternatives for parameters behind this boundary. private def signatureParameters(using rs: ResolutionContext) = rs.state.signatureParameters def registerParameterSignature(paramLists: Ls[ParamList], sign: Term)(using ResolutionContext): Unit = - def bind(paramLists: Ls[ParamList], tpe: DeclaredType)(using ResolutionContext): Unit = paramLists match + paramLists.flatMap(_.allParams).filter(_.sign.isEmpty).foreach: param => + val resolution = new TypeResolution(sign, messages => resolError(sign, messages)) + // Components can arrive later through aliases. Keep one endpoint for every + // parameter so existing observations and input constraints see all of them. + // Its templates may mention binders inside any quantified component. + rctx.recordTemplateBinders(resolution, N) + // Bottom contributes no values and keeps this accumulating union distinct + // from a single immutable reference that normalization could substitute away. + resolution.publish(TypeShape.Bottom) + val tpe = declaredType(resolution, Map.empty) + signatureParameters(param.sym) = tpe + listenTypeInstances(tpe): shape => + if param.sym.currentShapes.add(shape) then param.sym.notifyShapeListeners(shape) + // Check only signatures that explicitly specify one calling convention, + // following aliases and quantifiers. Compound signatures still contribute + // every component's flows, but rejecting one possible layout would turn this + // best-effort check into a false incompatibility. + def singleArrow(res: TypeResolution, seen: Set[TypeResolution])(using ResolutionContext): Bool = + if seen(res) then false else + def next(inner: TypeResolution): Bool = singleArrow(inner, seen + res) + res.currentShapes.toList match + case TypeShape.Function(_, _) :: Nil => true + case TypeShape.Reference(base, _, _, _) :: Nil => next(base) + case TypeShape.Contextual(ref) :: Nil => next(ref.tpe.resolution) + case TypeShape.Applied(base, _) :: Nil => next(base) + case TypeShape.Alias(_, S(rhs)) :: Nil => next(rhs) + case TypeShape.Polymorphic(_, _, body) :: Nil => next(body) + case _ => false + def bind(paramLists: Ls[ParamList], tpe: DeclaredType, checkLayout: Bool)(using ResolutionContext): Unit = paramLists match case Nil => () - case ps :: tail => listenTypeViews(tpe): + case ps :: tail => listenTypeViews(tpe, mergeIntersections = false): case Marked(callable: CallableTypeShape, marks) => val declared = callable.paramLists.head - if !declared.hasRest then ps.params.zip(declared.params).foreach: (param, sign) => - if param.sign.isEmpty then sign.foreach: tpe => - val contextual = transportType(tpe, marks :: Nil) - signatureParameters(param.sym) = contextual - listenTypeInstances(contextual): shape => - if param.sym.currentShapes.add(shape) then param.sym.notifyShapeListeners(shape) - callable.result.foreach(tpe => bind(tail, transportType(tpe, marks :: Nil))) + // Once alternatives have been approximated, a later curried list is + // still part of those alternatives, even if this component is one arrow. + val check = checkLayout && singleArrow(tpe.resolution, Set.empty) + if Arity(ps) != Arity(declared) then + if check then + rctx.report(ErrorReport( + msg"Implementation parameter list does not match its separate signature." -> ps.toLoc :: + msg"The signature has different required, optional, variadic, or trailing parameter positions." -> sign.toLoc :: Nil)) + val params = ps.allParams + zipDeclaredParameters(Arity(ps), declared): (index, tpe) => + val param = params(index) + val component = transportType(tpe, marks :: Nil) + if param.sign.isEmpty then + signatureParameters.get(param.sym).get.resolution.publish(TypeShape.Combined(TypeFormula.atom(component))) + else + // An inline annotation keeps its interface, but its inferred type + // arguments still receive constraints from the separate signature. + constrainParameter(param, instanceShape(component), Nil) + callable.result.foreach(tpe => bind(tail, transportType(tpe, marks :: Nil), check)) case _ => () - bind(paramLists, declaredType(typeResolution(sign), Map.empty)) + bind(paramLists, declaredType(typeResolution(sign), Map.empty), true) /** Synthesized constructor fields retain their source declaration on the symbol. */ private[semantics] def resultSignature(td: TermDefinition)(using ResolutionContext): Opt[Term] = td.tsym.decl match @@ -1520,24 +1640,10 @@ class NewResolver: * before a caller can substitute its own type argument. */ private def awaitedType(tpe: DeclaredType)(using ResolutionContext): DeclaredType = - rctx.state.awaitedTypes.getOrElseUpdate(tpe, { - val symbol = prelude.builtins.Awaited match - case symbol: TypeAliasSymbol => symbol - case _ => lastWords("The builtin Awaited must be an abstract type alias") - // The synthesized `Awaited` is always applied to the payload. Give it its own - // source, recorded as applied to one argument and written outside every - // binder. Reusing the payload's source would summarize it as a bare generic - // use in that source's scope (see dependencies), which is unknown whenever - // the source records no scope, and the promise would retain whole substitutions. - val head = Term.Ref(symbol)(new syntax.Tree.Ident(symbol.nme).withLocOf(tpe.resolution.source), N) - rctx.recordLexicalBinders(head, Set.empty) - rctx.recordAppliedTypeArguments(head, 1) - val constructor = new TypeResolution(head, tpe.resolution.fail) - constructor.publish(TypeShape.Alias(symbol, N)) - val resolution = new TypeResolution(tpe.resolution.source, tpe.resolution.fail) - resolution.publish(TypeShape.Applied(constructor, tpe.resolution :: Nil)) - declaredType(resolution, tpe) - }) + val symbol = prelude.builtins.Awaited match + case symbol: TypeAliasSymbol => symbol + case _ => lastWords("The builtin Awaited must be an abstract type alias") + appliedType(symbol, new ::(tpe, Nil)) private def listenAwaitedValue(value: TermShape, source: Term)(listener: Listener)(using ResolutionContext): Unit = listenInstanceViews(value)(view => listenAwaitedView(view, source)(listener)) @@ -1705,8 +1811,10 @@ class NewResolver: listenTypeViews(signature(td.sign.get, positive))(publish) else td.params match case params: NELs[ParamList] => publish(CallableTypeShape(source, - params.ne_map(ps => DeclaredParams(ps.params.map(_.sign.map(signature(_, !positive))), - ps.restParam.nonEmpty, ps.restParam.flatMap(_.sign).map(signature(_, !positive)))), + params.ne_map: ps => + def parameter(p: Param) = DeclaredParam(p.sign.map(signature(_, !positive)), p.flags) + DeclaredParams(ps.params.map(parameter), ps.variadic.map(_.map(parameter))) + , result, N, N, S(td), N)) case Nil if td.asyncAnnotation.nonEmpty => publish(promiseShape(td.asyncAnnotation.get, result.getOrElse(extremeType(true)))) @@ -1738,7 +1846,7 @@ class NewResolver: // type argument without leaking this abstract candidate into the caller. val site = FlowSymbol("generic interface") params.foreach: param => - val ref = SimpleRef(param)(param.id) + val ref = SimpleRef(param)(N) val unknown = RigidTypeShape(param, ref) publishParameter(param, MarkedShape.enter(unknown, ResolutionBoundary(owner), S(site))) @@ -1783,8 +1891,8 @@ class NewResolver: (using ResolutionContext): Unit = parameters.foreach: parameter => val instance = ContextualType(parameterType(parameter.parameter).instantiate(substitution), Nil) - parameter.lower.foreach(bound => constrainTypes(ContextualType(bound.instantiate(substitution), Nil), instance)) - parameter.upper.foreach(bound => constrainTypes(instance, ContextualType(bound.instantiate(substitution), Nil))) + parameter.lower.foreach(bound => constrainTypes(ContextualType(bound.instantiate(substitution), Nil), instance, N)) + parameter.upper.foreach(bound => constrainTypes(instance, ContextualType(bound.instantiate(substitution), Nil), N)) /** Consume the scheme at its authoritative site. Specialized values and curried * tails carry instantiated references and no scheme, so later calls reuse it. @@ -1801,7 +1909,7 @@ class NewResolver: def instantiate(tpe: DeclaredType): DeclaredType = tpe.instantiate(substitution) val instantiated = CallableTypeShape(callable.source, callable.paramLists.ne_map: params => - DeclaredParams(params.params.map(_.map(instantiate)), params.hasRest, params.rest.map(instantiate)) + params.mapTypes(instantiate) , callable.result.map(instantiate), N, N, callable.declaration, callable.asyncResult) // Reentrant observations must find the view before supplied arguments // can publish candidates or trigger another observation of this call. @@ -1823,23 +1931,26 @@ class NewResolver: resultSignature(definition).foreach: sign => val expected = declaredType(typeResolution(sign), Map.empty) listenResult(body): shape => - inferTypeArguments(expected.instantiate(rctx.instances), shape, Nil) + inferTypeArguments(expected.instantiate(rctx.instances), shape, Nil, S(body)) def checkAscription(body: Term, sign: Term)(using ResolutionContext): Unit = val expected = declaredType(typeResolution(sign), Map.empty) listenTerm(body): shape => - inferTypeArguments(expected.instantiate(rctx.instances), shape, Nil) + inferTypeArguments(expected.instantiate(rctx.instances), shape, Nil, S(body)) /** Keep both endpoints symbolic. Register before following either endpoint, * so recursive references reuse the relation even before any bound exists. */ - private[semantics] def constrainTypes(lower: ContextualType, upper: ContextualType)(using ResolutionContext): Unit = + private[semantics] def constrainTypes(lower: ContextualType, upper: ContextualType, site: Opt[Term])(using ResolutionContext): Unit = if rctx.state.typeRelations.add((lower, upper)) then instanceShape(lower.tpe).exit(lower.marks).enter(upper.marks) match - case value: TermShape => inferTypeArguments(upper.tpe, value, upper.marks) + case value: TermShape => inferTypeArguments(upper.tpe, value, upper.marks, site) case NoShape => () - private def inferTypeArguments(tpe: DeclaredType, value: TermShape, marks: Ls[Marks])(using ResolutionContext): Unit = + // Keep the first diagnostic use site outside the semantic constraint key, so + // reporting provenance does not multiply identical edges in the inference graph. + private def inferTypeArguments(tpe: DeclaredType, value: TermShape, marks: Ls[Marks], + site: Opt[Term])(using ResolutionContext): Unit = if !rctx.addTypeConstraint(tpe, value, marks) then return val annotation = tpe.resolution.source def follow(tpe: DeclaredType, args: TypeApplication, captures: Ls[Marks], @@ -1865,7 +1976,7 @@ class NewResolver: case Marked(instance: InstanceShape, context) => val endpoint = if context == NoMarks then Nil else context :: Nil value.exit(marks).enter(context) match - case actual: TermShape => inferTypeArguments(instance.tpe, actual, endpoint) + case actual: TermShape => inferTypeArguments(instance.tpe, actual, endpoint, site) case NoShape => () case _ => () else publishParameter(destination, lower) @@ -1877,24 +1988,11 @@ class NewResolver: case TypeShape.Applied(base, params) => follow(declaredType(base, tpe), typeApplication(params, tpe), captures, next) case TypeShape.Alias(symbol, S(rhs)) => - follow(declaredType(rhs, typeBindings(tpe, args, symbol.defn.get.tparams, true), tpe.positive), noTypeArguments, captures, next) + follow(declaredType(rhs, aliasBindings(tpe, args, symbol.defn.get.tparams, tpe.positive), tpe.positive), noTypeArguments, captures, next) case TypeShape.Hole(host) => value.enter(captures) match case value: TermShape => host.publish(value) case NoShape => () - case TypeShape.Tuple(fields) => observe: - case unknown @ Marked(_: UnknownValueShape, _) => - fields.foreach(field => inferTypeArguments(declaredType(field, tpe), unknown, marks)) - case Marked(tuple: TupleShape, context) => - fields.zipWithIndex.foreach: (field, index) => - tuple.getMember(index.toString) match - case MemberLookup.Indexed(actual, inner) => actual.foreach: element => - listenTupleField(element): shape => - shape.exit(inner).exit(context) match - case value: TermShape => - inferTypeArguments(declaredType(field, tpe), value, marks) - case NoShape => () - case _ => () - case _ => () + case TypeShape.Tuple(_) => observe(constrainTuple(tpe, _, marks, site)) case TypeShape.Union(left, right) => follow(declaredType(left, tpe), noTypeArguments, captures, next) follow(declaredType(right, tpe), noTypeArguments, captures, next) @@ -1907,8 +2005,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)) - case TypeShape.Function(_, _) => observe(constrainFunction(tpe, _, marks)) + case TypeShape.Record(_, fields) => observe(constrainRecord(fields.map((field, sign) => field -> declaredType(sign, tpe)), _, marks, site)) + case TypeShape.Function(_, _) => observe(constrainFunction(tpe, _, marks, site)) case TypeShape.Polymorphic(_, _, body) => follow(declaredType(body, tpe), args, captures, next) case TypeShape.Wildcard(input, _) => input.foreach(res => follow(declaredType(res, tpe.withPolarity(!tpe.positive)), args, captures, next)) @@ -1936,8 +2034,8 @@ class NewResolver: def constrain(param: TyParam, bound: DeclaredType, pattern: DeclaredType)(using ResolutionContext): Unit = listenArgumentParts(bound.instantiate(instances)): actual => listenArgumentParts(argumentType(pattern, param)): expected => - constrainTypes(ContextualType(actual.output, context), ContextualType(expected.output, Nil)) - constrainTypes(ContextualType(expected.input, Nil), ContextualType(actual.input, context)) + constrainTypes(ContextualType(actual.output, context), ContextualType(expected.output, Nil), site) + constrainTypes(ContextualType(expected.input, Nil), ContextualType(actual.input, context), site) def originalParameters()(using ResolutionContext): Unit = cls.tparams.zip(arguments).foreach: (param, pattern) => val bound = parameterType(TypeShape.Parameter(param.sym, param.sym.inferenceHost)) constrain(param, argumentType(bound, param), pattern) @@ -1952,7 +2050,7 @@ class NewResolver: case _: UnknownValueShape => cls.tparams.zip(arguments).foreach: (param, argument) => listenArgumentParts(argumentType(argument, param)): parts => - inferTypeArguments(parts.output, value, marks) + inferTypeArguments(parts.output, value, marks, site) case ds: DefnShape => if ds.clsDef.contains(cls) then originalParameters() else ds.ext.foreach(parent) case base: BaseShape => @@ -1969,51 +2067,109 @@ class NewResolver: // A concrete target stays fixed, but must receive bounds that arrive // through a symbolic source later. Concrete mismatch reporting is // separate from retaining this obligation in the graph. - case Marked(_: InstanceShape, _) => listenConstraintViews(value)(inferTypeArguments(tpe, _, marks)) + case Marked(_: InstanceShape, _) => listenConstraintViews(value)(inferTypeArguments(tpe, _, marks, site)) case _ => () // Alias expansion guards only unproductive cycles. Descending into a tuple, // nominal argument, or arrow installs another edge with its own cycle guard. withCanonicalType(tpe)(follow(_, noTypeArguments, Nil, Set.empty)) + /** Expand fixed tuple spreads before matching positions. Unknown-length + * segments share the remaining middle elements, as with declared parameter + * lists; their symbolic fields retain both input and output type arguments. + */ + private def constrainTuple(expected: DeclaredType, actual: TermShape, marks: Ls[Marks], + site: Opt[Term])(using ResolutionContext): Unit = + listenTypeViews(expected, mergeIntersections = false): + case Marked(tuple: TupleShape, context) => + def constrain(field: TupleShape.Fixed, value: TermShape)(using ResolutionContext): Unit = + listenTupleField(field): + case Marked(instance: InstanceShape, inner) => + inferTypeArguments(transportType(instance.tpe, inner :: context :: Nil), value, marks, site) + case _ => () + val (leading, rest) = tuple.segments.span(_.isInstanceOf[TupleShape.Fixed]) + val (trailing, middle) = rest.reverse.span(_.isInstanceOf[TupleShape.Fixed]) + val positions = leading ::: trailing.reverse + actual match + case unknown @ Marked(_: UnknownValueShape, _) => + tuple.segments.foreach(segment => constrain(segment.element, unknown)) + case Marked(supplied: TupleShape, suppliedContext) => + val arity = Arity(leading.length, leading.length, middle.nonEmpty, trailing.length) + zipTupleShapes(Nil, arity, supplied, suppliedContext): (index, shape) => + shape match + case value: TermShape if arity.hasRest && index == leading.length => + listenArrayElements(value): element => + middle.foreach(segment => constrain(segment.element, element)) + case value: TermShape => + val position = if arity.hasRest && index > leading.length then index - 1 else index + constrain(positions(position).element, value) + case NoShape => () + case _ => + listenArrayElements(actual): element => + tuple.segments.foreach(segment => constrain(segment.element, element)) + case _ => () + /** 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. */ private def constrainRecord(fields: Ls[(RcdField, DeclaredType)], - actual: TermShape, marks: Ls[Marks])(using ResolutionContext): Unit = + actual: TermShape, marks: Ls[Marks], site: Opt[Term])(using ResolutionContext): Unit = fields.foreach: (field, expected) => val flow = rctx.fieldProjectionSite(field.sym) - def receive(value: TermShape)(using ResolutionContext): Unit = inferTypeArguments(expected, value, marks) - def member(info: MemberLookup, instances: TypeSubstitution)(using ResolutionContext): Unit = info match - case MemberLookup.Contextual(source, substitution) => member(source, instances.withOverrides(substitution)) - 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)), field.rhs, _ => (), false)(using rctx.withInstances(instances)) - case MemberLookup.Declared(member, bound, inner, annotation, positive) => - listenDeclaredMember(member, bound, flow, field.rhs, 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))) - case MemberLookup.Dynamic(inner) => DynShape().exit(inner) match - case value: TermShape => receive(value) - case NoShape => () - case _ => () + def receive(value: TermShape)(using ResolutionContext): Unit = inferTypeArguments(expected, value, marks, site) + actual match case Marked(_: UnknownValueShape, _) => receive(actual) // A structural interface only describes public members. - case _ => member(actual.getMember(field.sym.nme).visibleFrom(_ => false, reportInaccessible = false), TypeSubstitution.empty) + case _ => listenMemberValue(actual.getMember(field.key.get).visibleFrom(_ => false, reportInaccessible = false), + flow, field.rhs, TypeSubstitution.empty, () => ())(receive) + + /** Read a member using the same declaration and scope rules for ordinary + * constraints and implicit protocol observations. The caller decides whether + * a missing/opaque member contributes uncertainty or no new constraint. + */ + private def listenMemberValue(info: MemberLookup, flow: FlowSymbol, source: Term, + instances: TypeSubstitution, unresolved: () => ResolutionContext ?=> Unit)(receive: Listener) + (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)) + 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))) + case MemberLookup.Dynamic(inner) => DynShape().exit(inner) match + case value: TermShape => receive(value) + case NoShape => () + case MemberLookup.Indexed(fields, inner) => fields.foreach: field => + listenTupleField(field): value => + transportShape(instantiateShape(value, instances), inner) match + case value: TermShape => receive(value) + case NoShape => () + case _ => unresolved() /** Function constraints reverse the parameter flow and preserve result flow. * Read an implementation only while checking it against its declared interface; * calls through that interface continue to expose only the declared result. */ - private[semantics] def constrainFunction(expected: DeclaredType, source: TermShape, marks: Ls[Marks])(using ResolutionContext): Unit = + private[semantics] def constrainFunction(expected: DeclaredType, source: TermShape, marks: Ls[Marks], + site: Opt[Term])(using ResolutionContext): Unit = val ShapeParts(actual, instances, _) = shapeParts(source) + def arityError(expected: DeclaredType)(using ResolutionContext): Unit = + val messages = + msg"Callback parameter list does not match its declared function type." -> actual.toLoc :: + msg"The expected function type is declared here." -> expected.resolution.source.toLoc :: Nil + site match + case S(use) => resolError(use, messages) + case N => rctx.report(ErrorReport(messages)) actual match case Marked(_: InstanceShape, _) => - listenConstraintViews(actual)(constrainFunction(expected, _, marks)) + listenConstraintViews(actual)(constrainFunction(expected, _, marks, site)) return case _ => () val context = actual.applicationHead._2 @@ -2027,72 +2183,169 @@ class NewResolver: case _ => () case _ => () def matchLists(expected: DeclaredType, remaining: Ls[(ParamList, Ls[Marks])])(using ResolutionContext): Unit = - listenTypeViews(expected): + listenTypeViews(expected, mergeIntersections = false): case Marked(callable: CallableTypeShape, expectedContext) => remaining match case (params, _) :: tail => val declared = callable.paramLists.head - if declared.params.length < params.params.length - || (declared.params.length > params.params.length && params.restParam.isEmpty) - || (declared.hasRest && params.restParam.isEmpty) then - resolError(expected.resolution.source, msg"Callback parameter list does not match its declared function type." -> actual.toLoc :: Nil) - else - params.params.zip(declared.params).foreach: (param, sign) => - sign.foreach: tpe => - listenTypeInstances(tpe): shape => - constrainParameterIn(param, shape.exit(expectedContext).enter(context), context, instances) - params.restParam.foreach: rest => - val fields = declared.params.drop(params.params.length).map: - case S(tpe) => TupleShape.TypedField(tpe, Nil) - case N => TupleShape.UnknownField(callable.source, Nil) - val suffix = if declared.hasRest - then TupleShape.Unknown(callable.source, TupleShape.UnknownField(callable.source, Nil)) :: Nil - else Nil - val tuple = TupleShape(callable.source, fields ::: suffix)(this) - constrainParameterIn(rest, tuple.exit(expectedContext).enter(context), context, instances) - callable.result.foreach: ret => - tail match - case Nil => results: shape => - shape.exit(context).enter(expectedContext) match - case value: TermShape => inferTypeArguments(ret, value, marks) - case NoShape => () - case _ => matchLists(ret, tail) + if callbackArityMismatch(Arity(params), declared) then + arityError(expected) + // Diagnostics must not disconnect parameter or result constraints. + val allParams = params.allParams + zipCallbackArguments(context, Arity(params), declared, callable.source, expectedContext)( + (index, shape) => constrainParameterIn(allParams(index), shape, context, instances, site)) + callable.result.foreach: ret => + tail match + case Nil => results: shape => + shape.exit(context).enter(expectedContext) match + case value: TermShape => inferTypeArguments(ret, value, marks, site) + case NoShape => () + case _ => matchLists(ret, tail) case Nil => () case Marked(record: RecordTypeShape, _) => constrainRecord(record.fields.iterator.map((name, tpe) => record.declarations(name) -> tpe).toList, - actual, marks) + actual, marks, site) case _ => () actual match case Marked(_: UnknownValueShape, _) => - listenTypeViews(expected): + listenTypeViews(expected, mergeIntersections = false): case Marked(callable: CallableTypeShape, _) => - callable.result.foreach(ret => inferTypeArguments(ret, actual, marks)) + callable.result.foreach(ret => inferTypeArguments(ret, actual, marks, site)) case _ => () case Marked(givenType: CallableTypeShape, actualContext) => - listenTypeViews(expected): + listenTypeViews(expected, mergeIntersections = false): case Marked(wanted: CallableTypeShape, expectedContext) => val domain = givenType.paramLists.head val expectedDomain = wanted.paramLists.head - if domain.params.length != expectedDomain.params.length || domain.hasRest != expectedDomain.hasRest then - resolError(expected.resolution.source, msg"Callback parameter list does not match its declared function type." -> actual.toLoc :: Nil) - else - domain.params.zip(expectedDomain.params).foreach: - case (S(givenParam), S(wantedParam)) => - listenTypeInstances(wantedParam): shape => - shape.exit(expectedContext).enter(actualContext) match - case value: TermShape => inferTypeArguments(givenParam, value, context) - case NoShape => () - case _ => () - wanted.result.foreach: ret => - def receive(shape: TermShape)(using ResolutionContext): Unit = shape.exit(actualContext).enter(expectedContext) match - case value: TermShape => inferTypeArguments(ret, value, marks) + if callbackArityMismatch(Arity(domain), expectedDomain) then + arityError(expected) + val allParams = domain.allParams + zipCallbackArguments(actualContext :: Nil, Arity(domain), expectedDomain, wanted.source, expectedContext)( + (index, shape) => allParams(index).tpe.foreach: tpe => + shape match + case value: TermShape => inferTypeArguments(tpe, value, context, site) case NoShape => () - listenCallableResult(givenType)(receive) + ) + wanted.result.foreach: ret => + def receive(shape: TermShape)(using ResolutionContext): Unit = shape.exit(actualContext).enter(expectedContext) match + case value: TermShape => inferTypeArguments(ret, value, marks, site) + case NoShape => () + listenCallableResult(givenType)(receive) case _ => () case _ => matchLists(expected, actual.unappliedParams) + /** Check obvious arity and droppability mismatches. Different variadic layouts + * need not agree: a callback may collect trailing arguments in its rest parameter + * or take fixed interface arguments from the end. More involved compatibility + * questions belong to type checking, rather than a structural-equality check. + * For fixed callbacks, diagnose trailing inputs that remain out of reach even + * when callers omit all optional leading and variadic arguments. + */ + private def callbackArityMismatch(actual: Arity, expected: DeclaredParams): Bool = + val trailingCapacity = (actual.leading - expected.leadingRequired).max(0) + actual.required > Arity(expected).required + || !actual.hasRest && expected.variadic.exists(!_.param.flags.droppable) + || !actual.hasRest && expected.params.drop(actual.leading).exists(!_.flags.droppable) + || !actual.hasRest && expected.trailing.drop(trailingCapacity).exists(!_.flags.droppable) + + /** Approximate a declared argument list without choosing a compound signature's + * branch or enumerating spread lengths. Equal layouts retain whole parameter + * annotations. Otherwise align a guaranteed prefix and suffix, and join all + * remaining flows at every possible middle position. In particular, an unknown + * position must not erase a known element type or a symbolic inference endpoint. + */ + private def zipDeclaredParameters(arity: Arity, declared: DeclaredParams)( + publish: (Int, DeclaredType) => ResolutionContext ?=> Unit)(using ResolutionContext): Unit = + def assign(index: Int, param: DeclaredParam): Unit = param.tpe.foreach(publish(index, _)) + if arity == Arity(declared) then + declared.allParams.zipWithIndex.foreach((sign, index) => assign(index, sign)) + else + val sharedTail = arity.trailing.min(declared.trailing.length) + val alignedTail = arity.trailing == declared.trailing.length + val prefix = if alignedTail then arity.leading.min(declared.params.length) + else arity.leadingRequired.min(declared.leadingRequired) + declared.params.take(prefix).zipWithIndex.foreach((sign, index) => assign(index, sign)) + declared.trailing.takeRight(sharedTail).zipWithIndex.foreach: (sign, index) => + assign(arity.leading + 1 + arity.trailing - sharedTail + index, sign) + val positions = (prefix until arity.leading) ++ + (arity.leading + 1 until arity.leading + 1 + arity.trailing - sharedTail) + val middle = declared.params.drop(prefix) ::: declared.trailing.dropRight(sharedTail) + middle.foreach(sign => positions.foreach(assign(_, sign))) + def restElement(element: DeclaredType, marks: Marks)(using ResolutionContext): Unit = + val tpe = transportType(element, marks :: Nil) + positions.foreach(index => publish(index, tpe)) + if arity.hasRest then publish(arity.leading, arrayType(tpe)) + declared.rest.foreach: rest => + listenTypeViews(rest, mergeIntersections = false): + case Marked(array: NominalInstanceView, marks) => + arrayElementType(array).foreach(restElement(_, marks)) + case Marked(tuple: TupleShape, marks) => + tuple.segments.foreach: segment => + listenTupleField(segment.element): + // Written tuple types retain their symbolic element endpoints. + case Marked(instance: InstanceShape, inner) => + restElement(transportType(instance.tpe, inner :: Nil), marks) + case _ => arrayElementType(tuple.arrayParent).foreach(restElement(_, marks)) + case _ => () + if arity.hasRest then + // Slicing can change a tuple-typed rest's length. Keep its element types + // above, but widen the length to avoid false arity errors on forwarding. + // Scalar middle inputs likewise contribute an Array of their declared type. + middle.flatMap(_.tpe).foreach(tpe => publish(arity.leading, arrayType(tpe))) + if middle.flatMap(_.tpe).isEmpty then + // Even an empty or still-unresolved rest supplies the Array interface. + publish(arity.leading, arrayType(extremeType(false))) + + private def arrayType(element: DeclaredType)(using ResolutionContext): DeclaredType = + appliedType(prelude.builtins.Array, new ::(element, Nil)) + + /** Synthesized applications need stable resolution identities: reference interning + * cannot merge newly allocated TypeResolution nodes. Share the entire application + * by constructor and symbolic arguments, rather than making a cache per builtin. + * Combined atoms embed each argument's bindings, polarity, and scope transfers; + * observing its current values here would lose later inference flows. + */ + private def appliedType(symbol: ClassSymbol | TypeAliasSymbol, arguments: NELs[DeclaredType]) + (using ResolutionContext): DeclaredType = + rctx.state.appliedTypes.getOrElseUpdate((symbol, arguments), { + val source = arguments.head.resolution + val head = Term.Ref(symbol)(new Tree.Ident(symbol.nme).withLocOf(source.source), N) + // This head is fully applied and has no lexical binders. Reusing an argument's + // source would misclassify it as a bare generic use in that argument's scope. + rctx.recordLexicalBinders(head, Set.empty) + rctx.recordAppliedTypeArguments(head, arguments.length) + val constructor = new TypeResolution(head, source.fail) + softAssert(symbol.defn.get.tparams.length == arguments.length, "A synthesized type application must supply every parameter") + symbol match + case cls: ClassSymbol => constructor.publish(TypeShape.Nominal(cls.defn.get)) + case alias: TypeAliasSymbol => + constructor.publish(TypeShape.Alias(alias, alias.defn.get.rhs.map(typeResolution))) + val args = arguments.map: argument => + val resolution = new TypeResolution(argument.resolution.source, argument.resolution.fail) + resolution.publish(TypeShape.Combined(TypeFormula.atom(argument))) + resolution + val resolution = new TypeResolution(source.source, source.fail) + resolution.publish(TypeShape.Applied(constructor, args)) + declaredType(resolution, Map.empty) + }) + + /** Fixed callback interfaces describe an ordinary argument tuple. Variable + * layouts use the same flow approximation as separate signatures. + */ + private def zipCallbackArguments(mss: Ls[Marks], arity: Arity, declared: DeclaredParams, source: Term, marks: Marks)( + publish: (Int, TermShape | NoShape) => ResolutionContext ?=> Unit)(using ResolutionContext): Unit = + def fields(params: Ls[DeclaredParam]): Ls[TupleShape.Fixed] = params.map: param => + param.tpe match + case S(tpe) => TupleShape.TypedField(tpe, Nil) + case N => TupleShape.UnknownField(source, Nil) + if !declared.hasRest && declared.leadingRequired == declared.params.length && arity.required <= declared.params.length then + zipTupleShapes(mss, arity, TupleShape(source, fields(declared.params))(this), marks)(publish) + else + zipDeclaredParameters(arity, declared): (index, tpe) => + listenTypeInstances(tpe)(shape => publish(index, shape.exit(marks).enter(mss))) + private def listenSignatureResult(tpe: DeclaredType, count: Int)(listener: Listener)(using ResolutionContext): Unit = if count == 0 then listenTypeInstances(tpe)(listener) - else listenTypeViews(tpe): + else listenTypeViews(tpe, mergeIntersections = false): case Marked(callable: CallableTypeShape, marks) => callable.result.foreach(tpe => listenSignatureResult(transportType(tpe, marks :: Nil), count - 1)(listener)) case _ => () @@ -2168,14 +2421,14 @@ class NewResolver: * through one path then cross the same scope twice when they are read * through the other path. */ - private[semantics] def nominalMember(view: NominalInstanceView, name: Str, receiver: Marks)(using ResolutionContext): MemberLookup = + private[semantics] def nominalMember(view: NominalInstanceView, name: MemberKey, receiver: Marks)(using ResolutionContext): MemberLookup = val defn = view.defn // Module/object references introduce no value invocation boundary. val scope = defn.sym match case _: ModuleOrObjectSymbol => N case symbol => S(ResolutionBoundary(symbol)) def inherited: MemberLookup = - (name.toIntOption, arrayElementType(view)) match + (MemberKey.index(name), arrayElementType(view)) match // An element's value keeps its whole path, including its provenance. case (S(index), S(element)) if index >= 0 => MemberLookup.Indexed(TupleShape.TypedField(element, Nil) :: Nil, Nil).withMarks(receiver :: Nil) @@ -2192,7 +2445,7 @@ class NewResolver: // applying these marks to its members yields no candidates. case NoShape => parentView.getMember(name).withMarks(inner :: receiver :: Nil) parentMember.withAnnotation(view.annotation) - defn.body.members.get(name) match + MemberKey.own(defn, name) match case S(member) => val (location, provenance) = splitReceiverPath(defn, receiver) // Supplied arguments are outside the class; its parameter annotations @@ -2301,7 +2554,7 @@ class NewResolver: case nominal: NominalInstanceView => arrayElementType(nominal).foreach: element => listenTerm(rhs): value => value.enter(context) match - case value: TermShape => inferTypeArguments(element, value, context) + case value: TermShape => inferTypeArguments(element, value, context, S(rhs)) case NoShape => () case _ => () @@ -2317,9 +2570,10 @@ class NewResolver: case TupleShape.ViewedField(source, instances, _) => listenTupleField(source)(value => receive(instantiateShape(value, instances)))(using rctx.withInstances(instances)) - /** Array spreads have unknown length, but their declared element type still - * constrains every element. Never recover elements from constructor values: - * Arrays are mutable, and the one-number constructor creates empty slots. + /** Array patterns and rest-parameter constraints read Array's element bound. + * Spreads instead observe the iteration protocol below. Never recover array + * elements from constructor values: arrays are mutable, and the one-number + * constructor creates empty slots. */ private[semantics] def listenArrayElements(shape: TermShape)(listener: Listener)(using ResolutionContext): Bool = shape.applicationHead._1 match @@ -2330,6 +2584,52 @@ class NewResolver: case NoShape => ()) case _ => listenClassArgument(shape, prelude.builtins.Array.defn.get)(listener) + /** Follow the actual iteration protocol. Each spread owns stable selection and + * application sites; all operand alternatives contribute to one element host. + * Unknown steps stay permissive and never replace already-known element flows. + */ + private def iterationFlow(source: Term, scope: Opt[Set[VarSymbol]])(using ResolutionContext): IterationFlow = + rctx.spreadFlow(source): + def site(name: Str): FlowSymbol = FlowSymbol(name)(using rctx.owner) + val empty = Tup(Nil)(Tree.Tup(Nil)).withLocOf(source) + scope.foreach(rctx.recordLexicalBinders(empty, _)) + def call(name: Str): App = + val tree = new Tree.App(Tree.Ident(name), Tree.Tup(Nil)) + tree.withLocOf(source) + val result = new App(Term.Missing, empty)(tree, N, site(name)) + scope.foreach(rctx.recordLexicalBinders(result, _)) + result + val elements = new TypeResolution(source, messages => resolError(source, messages)) + rctx.recordTemplateBinders(elements, scope) + val flow = new IterationFlow(elements, call("iterator"), call("next"), + site("iterator member"), site("next member"), site("iteration done"), site("iteration value")) + def select(value: TermShape, key: MemberKey, site: FlowSymbol)(receive: Listener)(using ResolutionContext): Unit = + listenInstanceViews(value): view => + listenMemberValue(view.getMember(key).visibleFrom(_ => false, reportInaccessible = false), + site, source, TypeSubstitution.empty, () => receive(UnknownValueShape.spread(source, view)))(receive) + listenTerm(flow.iterator): iterator => + select(iterator, "next", flow.nextMember)(applyShape(_, empty, flow.next, checked = false)) + listenTerm(flow.next): step => + select(step, "done", flow.doneMember): done => + val completed = done match + case Marked(intro: IntroShape, _) => intro.trm match + case Lit(Tree.BoolLit(true)) => true + case _ => false + case _ => false + if !completed then + select(step, "value", flow.valueMember)(value => elements.publish(TypeShape.Inferred(value))) + flow + + private def listenIterableElements(value: TermShape, flow: IterationFlow)(using ResolutionContext): Unit = + val symbols = prelude.builtins.SymbolModule + val iteratorKey = SymbolKey(symbols.module :: symbols.iterator :: Nil)( + symbols.bms.ref().synthSel(symbols.iterator)) + listenInstanceViews(value): view => + listenMemberValue(view.getMember(iteratorKey).visibleFrom(_ => false, reportInaccessible = false), + flow.iteratorMember, flow.elements.source, TypeSubstitution.empty, + () => flow.elements.publish(TypeShape.Inferred(UnknownValueShape.spread(flow.elements.source, view)))): + method => applyShape(method, flow.iterator.rhs, flow.iterator, checked = false) + private[semantics] def listenPromisePayload(shape: TermShape)(listener: Listener)(using ResolutionContext): Bool = listenClassArgument(shape, prelude.builtins.Promise.defn.get)(listener) @@ -2363,96 +2663,125 @@ class NewResolver: /** Subscribe once to complete argument-tuple candidates. A pending spread is not * an argument, nor evidence of an arity mismatch; each resolved combination is. */ - def zipArgs(mss: Ls[Marks], ps: Ls[Param], r: Opt[Param], args: Term, src: Term, funSh: TermShape)(using ResolutionContext): Unit = - zipArgsIn(mss, ps, r, args, src, funSh, rctx.instances) + def zipArgs(mss: Ls[Marks], ps: ParamList, args: Term, src: Term, funSh: TermShape)(using ResolutionContext): Unit = + zipArgsIn(mss, ps, args, src, funSh, rctx.instances) - private def zipArgsIn(mss: Ls[Marks], ps: Ls[Param], r: Opt[Param], args: Term, src: Term, funSh: TermShape, + private def zipArgsIn(mss: Ls[Marks], ps: ParamList, args: Term, src: Term, funSh: TermShape, instances: TypeSubstitution)(using ResolutionContext): Unit = - val params = ps ::: r.toList - zipArgumentShapes(mss, ps.length, r.nonEmpty, args, src, funSh): (index, shape) => - constrainParameterIn(params(index), shape, mss, instances) + val params = ps.allParams + zipArgumentShapes(mss, Arity(ps), args, src, funSh)( + (index, shape) => constrainParameterIn(params(index), shape, mss, instances, S(src))) private[semantics] def constrainParameter(p: Param, shape: TermShape | NoShape, marks: Ls[Marks])(using ResolutionContext): Unit = - constrainParameterIn(p, shape, marks, rctx.instances) + constrainParameterIn(p, shape, marks, rctx.instances, N) private def constrainParameterIn(p: Param, shape: TermShape | NoShape, marks: Ls[Marks], - instances: TypeSubstitution)(using ResolutionContext): Unit = - shape match - case NoShape => () - case sh: TermShape => - parameterSignature(p) match - case S(tpe) => inferTypeArguments(tpe.instantiate(instances), sh, marks) - case N => publishParameter(p.sym, sh)(using rctx.withInstances(instances)) + instances: TypeSubstitution, site: Opt[Term])(using ResolutionContext): Unit = shape match + case NoShape => () + case sh: TermShape => + parameterSignature(p) match + case S(tpe) => inferTypeArguments(tpe.instantiate(instances), sh, marks, site) + case N => publishParameter(p.sym, sh)(using rctx.withInstances(instances)) private[semantics] def parameterSignature(p: Param)(using ResolutionContext): Opt[DeclaredType] = p.sign.map(sign => declaredType(typeResolution(sign), Map.empty)).orElse(signatureParameters.get(p.sym)) .orElse(p.sym.getState.newResolverState.signatureParameters.get(p.sym)(using p.sym.getState.newResolverState.context)) - private def zipArgumentShapes(mss: Ls[Marks], expectedCount: Int, hasRest: Bool, + /** The number of arguments a parameter list accepts, with `leading` parameters, of which + * `leadingRequired` are required, and, if `hasRest`, a variadic parameter followed by `trailing` ones. */ + private case class Arity(leadingRequired: Int, leading: Int, hasRest: Bool, trailing: Int): + require(hasRest || trailing == 0) + def required: Int = leadingRequired + trailing + def describe: Message = + if hasRest then msg"at least ${required.toString} ${"argument".pluralized(required)}" + else if required == leading then msg"${required.toString} ${"argument".pluralized(required)}" + else msg"${required.toString} to ${leading.toString} arguments" + private object Arity: + def apply(ps: ParamList): Arity = Arity(ps.leadingRequiredCount, ps.params.length, ps.variadic.nonEmpty, ps.trailingParams.length) + def apply(ps: DeclaredParams): Arity = Arity(ps.leadingRequired, ps.params.length, ps.hasRest, ps.trailing.length) + + /** Publish the shapes of the arguments matching each parameter position, where index + * `arity.leading` denotes the variadic parameter and the following indices its trailing parameters, + * as in `ParamList.allParams`. Optional parameters already include null at their declarations. */ + private def zipArgumentShapes(mss: Ls[Marks], arity: Arity, args: Term, src: Term, funSh: TermShape)(publish: (Int, TermShape | NoShape) => ResolutionContext ?=> Unit)(using ResolutionContext): Unit = - def matchSegments(tuple: TupleShape, marks: Marks)(using ResolutionContext): Unit = - val segments = tuple.segments - val knownCount = segments.count(_.isInstanceOf[TupleShape.Fixed]) - // Distribute only successful argument counts. Unknown spreads retain all - // possible fixed positions and the residual tail for a rest parameter. - val extra = (expectedCount - knownCount).max(0) + checkArgumentArity(args, arity, src, funSh): + case (tuple, marks) => zipTupleShapes(mss, arity, tuple, marks)(publish) + + /** Calls and tuple patterns match the same leading/rest/trailing layout. + * Share position bounds and rest slicing so both preserve possible elements + * without assigning them impossible positions or inventing minimum lengths. + */ + private def zipTupleShapes(mss: Ls[Marks], arity: Arity, tuple: TupleShape, marks: Marks)( + publish: (Int, TermShape | NoShape) => ResolutionContext ?=> Unit)(using ResolutionContext): Unit = + val Arity(leadingRequired, expectedCount, hasRest, trailing) = arity + val knownCount = tuple.segments.count(_.isInstanceOf[TupleShape.Fixed]) + val unknownCount = tuple.segments.length - knownCount + val minExtra = (arity.required - knownCount).max(0) + val extra = (expectedCount - knownCount).max(0) + // Match leading positions from the left and trailing positions from the right. + // A fixed field cannot cross the required parameters at the opposite end. + // When all unknown segments precede it, they must supply any missing required + // arguments. These bounds avoid enumerating spread lengths or their combinations. + def assignPositions(segments: Ls[TupleShape.Segment], count: Int, reserved: Int, position: Int => Int): Unit = def assign(segment: TupleShape.Segment, positions: Range)(using ResolutionContext): Unit = positions.foreach: index => listenTupleField(segment.element): sh => - publish(index, sh.exit(marks).enter(mss)) - def loop(rest: Ls[TupleShape.Segment], before: Int, unknownBefore: Bool)(using ResolutionContext): Unit = rest match + publish(position(index), sh.exit(marks).enter(mss)) + def loop(rest: Ls[TupleShape.Segment], before: Int, unknownBefore: Int): Unit = rest match case Nil => () case (field: TupleShape.Fixed) :: tail => - val unknownAfter = tail.exists(_.isInstanceOf[TupleShape.Unknown]) - // With no later unknown segment, preceding spreads must supply all - // missing fixed arguments, even when the function has a rest parameter. - val first = if unknownBefore && !unknownAfter then before + extra else before - val last = if !unknownBefore then before - else if hasRest then expectedCount - 1 else before + extra - assign(field, first.max(0) until (last + 1).min(expectedCount)) + val unknownAfter = unknownCount - unknownBefore + val after = knownCount - before - 1 + if unknownAfter > 0 || after >= reserved then + val first = if unknownBefore > 0 && unknownAfter == 0 then before + minExtra else before + val last = if unknownBefore == 0 then before + else if hasRest then count - 1 else before + extra + assign(field, first until (last + 1).min(count)) loop(tail, before + 1, unknownBefore) case (unknown: TupleShape.Unknown) :: tail => - val end = if hasRest then expectedCount else before + extra + val end = if hasRest then count else (before + extra).min(count) assign(unknown, before until end) - loop(tail, before, true) - loop(segments, 0, false) - if hasRest then - // If consumption reaches an unknown segment, some subsequent fields - // may have been consumed too. Approximate that optional prefix with an - // unknown segment, retaining the suffix that must remain. Publishing - // separate concrete tails here would turn uncertainty into false arity - // failures when the rest tuple is spread into another call. - def drop(xs: Ls[TupleShape.Segment], count: Int, approximate: Bool)(using ResolutionContext): Ls[TupleShape.Segment] = - if count == 0 then xs - else xs match - case Nil => Nil - case (_: TupleShape.Fixed) :: rest => drop(rest, count - 1, approximate) - case (unknown: TupleShape.Unknown) :: Nil => - // Consuming a prefix changes the length, but not the element - // shape, when there are no following fields to merge into it. - if approximate then unknown :: Nil else Nil - case (_: TupleShape.Unknown) :: rest => - val suffix = drop(rest, count, false) - if approximate then TupleShape.Unknown(tuple.source, TupleShape.UnknownField(tuple.source, Nil)) :: suffix else suffix - val remaining = drop(segments, expectedCount, true) - val rest = if expectedCount == 0 then tuple else TupleShape.restView(tuple, remaining)(this) - publish(expectedCount, rest.exit(marks).enter(mss)) - checkArgumentArity(args, expectedCount, hasRest, src, funSh): - case (tuple, marks) => matchSegments(tuple, marks) + loop(tail, before, unknownBefore + 1) + loop(segments, 0, 0) + assignPositions(tuple.segments, expectedCount, trailing, identity) + assignPositions(tuple.segments.reverse, trailing, leadingRequired, index => expectedCount + trailing - index) + if hasRest then + // If consumption reaches an unknown segment, some subsequent fields + // may have been consumed too. Widen their lengths, keeping every field's + // deferred element flow. Replacing them with an opaque unknown would lose + // member targets and ambiguities; retaining them as fixed fields would + // invent a minimum length when the rest is spread into another call. + def drop(xs: Ls[TupleShape.Segment], count: Int, approximate: Bool)(using ResolutionContext): Ls[TupleShape.Segment] = + if count == 0 then xs + else xs match + case Nil => Nil + case (_: TupleShape.Fixed) :: rest => drop(rest, count - 1, approximate) + case (unknown: TupleShape.Unknown) :: Nil => + // Consuming a prefix changes the length, but not the element + // shape, when there are no following fields to merge into it. + if approximate then unknown :: Nil else Nil + case (_: TupleShape.Unknown) :: rest => + val suffix = drop(rest, count, false) + if approximate then + xs.take(xs.length - suffix.length).map(segment => TupleShape.Unknown(tuple.source, segment.element)) ::: suffix + else suffix + val remaining = drop(drop(tuple.segments, expectedCount, true).reverse, trailing, true).reverse + val rest = if remaining == tuple.segments then tuple else TupleShape.restView(tuple, remaining)(this) + publish(expectedCount, rest.exit(marks).enter(mss)) - private def checkArgumentArity(args: Term, expected: Int, rest: Bool, src: Term, + private def checkArgumentArity(args: Term, arity: Arity, src: Term, callable: TermShape)(matched: ShapeListener[(TupleShape, Marks)])(using ResolutionContext): Unit = val reportKey = new Object listenTermViews(args): case Marked(tuple: TupleShape, marks) => val known = tuple.segments.count(_.isInstanceOf[TupleShape.Fixed]) val unknown = known != tuple.segments.length - val mismatch = (!unknown && known < expected) || (!rest && known > expected) + val mismatch = (!unknown && known < arity.required) || (!arity.hasRest && known > arity.leading) if mismatch then if rctx.state.reportedArities.getOrElseUpdate(reportKey, mutable.Set.empty).add(known) then val count = if unknown then msg"at least ${known}" else msg"${known}" - resolError(src, callable.arityDiagnostic(msg"${callable.describe.capitalize} expected ${expected} ${ - "argument".pluralized(expected)}, but got ${count}")) + resolError(src, callable.arityDiagnostic(msg"${callable.describe.capitalize} expected ${arity.describe}, but got ${count}")) else matched((tuple, marks)) case _ => resolError(src, msg"Expected an argument tuple." -> args.toLoc :: Nil) @@ -2486,7 +2815,7 @@ class NewResolver: case S(ps) => // Elaboration reports and removes rest parameters of class-like definitions, // so the arity of a constructor pattern is the number of plain parameters. - softAssert(ps.restParam.isEmpty, "Unexected rest parameter in class-like definition") + softAssert(ps.variadic.isEmpty, "Unexected rest parameter in class-like definition") if args.sizeCompare(ps.params) =/= 0 then rctx.markError(res) resolError(res, @@ -2562,8 +2891,7 @@ class NewResolver: * supply undeclared assumptions to guards or transformations. */ def checkPatternDefinition(definition: PatternDef)(using ResolutionContext): Unit = - val source = Term.MemberRef(definition.bsym)(new syntax.Tree.Ident(definition.bsym.nme).withLocOf(definition), - FlowSymbol.pat()(using rctx.owner)) + val source = Term.MemberRef(definition.bsym)(N, FlowSymbol.pat()(using rctx.owner)) val unknown = UnknownValueShape(source, fromPublicInterface = false)(ShapeProvenance( msg"Pattern '${definition.sym.nme}' accepts values of unknown shape." -> definition.headerLoc :: Nil)) matchShapePat(unknown, definition.pattern)(_ => ()) @@ -2677,7 +3005,7 @@ class NewResolver: // The transform is lowered to a separate lambda. Its source references // already name these parameter symbols, not the aliases used by guards. parameters.foreach: (binding, parameter) => - pipeTerm(Term.SimpleRef(binding)(binding.id), parameter) + pipeTerm(Term.SimpleRef(binding)(N), parameter) matchShapePat(shape, pat)(_ => ()) listenTerm(transform)(matched) case Pattern.Guarded(pat, _) => matchShapePat(shape, pat)(matched) @@ -2688,8 +3016,10 @@ class NewResolver: case CtorPatternShape(cls, fs, _, resSym, constructorMarks) => // Nominal views already live outside the instance. Cancel the // constructor's own exit, retaining its enclosing lexical path. + // A self-contained value such as unit would discard those outer + // exits and lose the context of a class returned from a function. val context = cls match - case _: ClassDef => MarkedShape.enter(unitResultShape, ResolutionBoundary(cls.sym), N).exit(constructorMarks) match + case _: ClassDef => MarkedShape.enter(markCarrier, ResolutionBoundary(cls.sym), N).exit(constructorMarks) match case Marked(_, marks) => marks :: Nil case NoShape => lastWords("A wildcard constructor entry cannot reject its own exit") case _: ModuleOrObjectDef => Nil @@ -2702,6 +3032,7 @@ class NewResolver: if sh.isInstanceOfClass(cls) then fs.foreach: (bms, pat) => def member(info: MemberLookup, instances: TypeSubstitution)(using ResolutionContext): Unit = info match + case MemberLookup.All(members) => members.foreach(member(_, instances)) case MemberLookup.Contextual(source, substitution) => member(source, instances.withOverrides(substitution)) case MemberLookup.Found(sym: BlockMemberSymbol, marks) => fromBMS(sym, resSym, marks, @@ -2801,41 +3132,11 @@ class NewResolver: val known = segments.count(_.isInstanceOf[TupleShape.Fixed]) val uncertain = known != segments.length if (uncertain || known >= required) && (spread.nonEmpty || known <= required) then - def bind(segment: TupleShape.Segment, p: Pattern)(using ResolutionContext): Unit = - def receive(value: TermShape)(using ResolutionContext): Unit = value.exit(marks) match - case value: TermShape => matchShapePat(value, p)(_ => ()) - case NoShape => () - listenTupleField(segment.element)(receive) - // A spread can move subsequent fields. Retain every possible position - // rather than treating the first unknown segment as the only candidate. - def positions(xs: Ls[TupleShape.Segment], ps: Ls[Pattern])(using ResolutionContext): Unit = - var before = 0 - var unknownBefore = false - xs.foreach: - case field: TupleShape.Fixed => - ps.iterator.zipWithIndex.foreach: (p, index) => - if index >= before && (if !unknownBefore then index == before - else spread.nonEmpty || index <= before + required - known) then bind(field, p) - before += 1 - case unknown: TupleShape.Unknown => - ps.drop(before).foreach(bind(unknown, _)) - unknownBefore = true - positions(segments, leading) - positions(segments.reverse, trailing.reverse) - spread.foreach: (_, p, _) => - // Stop trimming at an unknown segment: its possible lengths include - // consuming the remaining prefix/suffix, so all later values may remain. - def trim(xs: Ls[TupleShape.Segment], count: Int): Ls[TupleShape.Segment] = - if count == 0 then xs else xs match - case (_: TupleShape.Fixed) :: tail => trim(tail, count - 1) - case _ => xs - val middle = trim(trim(segments, leading.length).reverse, trailing.length).reverse - // Reusing an unchanged view also bounds recursive tail matching on - // arrays with unknown length; wrapping it again would grow forever. - val rest = if middle == segments then tuple - else TupleShape.restView(tuple, middle)(this) - rest.exit(marks) match - case value: TermShape => matchShapePat(value, p)(_ => ()) + val patterns = leading ::: spread.toList.flatMap((_, rest, trailing) => rest :: trailing) + val arity = Arity(leading.length, leading.length, spread.nonEmpty, trailing.length) + zipTupleShapes(Nil, arity, tuple, marks): (index, value) => + value match + case value: TermShape => matchShapePat(value, patterns(index))(_ => ()) case NoShape => () tuple.exit(marks) match case value: TermShape => matched(value) @@ -2896,10 +3197,13 @@ class NewResolver: listenTypeArgument(parameter): bound => bound.applicationHead._1 match case _: RigidTypeShape => () - case _ => inferTypeArguments(instanceType(instance), instantiateShape(bound, substitution), marks) + case _ => inferTypeArguments(instanceType(instance), instantiateShape(bound, substitution), marks, N) substitution def appShape(source: TermShape, args: Term, res: App)(using ResolutionContext): Unit = + applyShape(source, args, res, checked = true) + + private def applyShape(source: TermShape, args: Term, res: App, checked: Bool)(using ResolutionContext): Unit = val callerInstances = rctx.instances val ShapeParts(lhs, lexical, supplied) = shapeParts(source) val captured = callerInstances.withOverrides(lexical) @@ -2912,25 +3216,33 @@ class NewResolver: def publish(value: TermShape)(using ResolutionContext): Unit = publishViewed(res, value, instances)(using rctx.withInstances(callerInstances)) lhs match + case Marked(view: IntersectionView, marks) => + view.values.foreach: value => + transportShape(value, marks :: Nil) match + case value: TermShape => applyShape(value, args, res, checked) + case NoShape => () + return case Marked(_: InstanceShape, _) => - listenInstanceViews(instantiateShape(lhs, instances))(appShape(_, args, res)) + listenInstanceViews(instantiateShape(lhs, instances), mergeIntersections = checked)(applyShape(_, args, res, checked)) return case _ => () - // Only explicit dynamic values authorize calls without a known interface. - // An unknown alternative must invalidate the call even if other candidates - // happen to be callable in this compilation unit. + // Explicit calls require a known interface or an explicitly dynamic value. + // Implicit protocol observations instead retain unknown results below and + // leave protocol compatibility to type checking. lhs match case Marked(_: DynShape, _) => + if checked then res.callableTargets.currentShapes.add(source) publish(lhs) return case _ => () lhs match case Marked(nominal: NominalInstanceView, _) if nominal.isInstanceOfClass(prelude.builtins.Function.defn.get) => + if checked then res.callableTargets.currentShapes.add(source) // The builtin Function class guarantees callability, including through // aliases and subclasses, but supplies neither an arity nor a result // interface. Check the argument tuple and leave arity checks to runtime; // the result must remain unknown rather than acquiring dynamic access. - checkArgumentArity(args, 0, true, res, lhs)(_ => ()) + if checked then checkArgumentArity(args, Arity(0, 0, true, 0), res, lhs)(_ => ()) // The call identifies the result in the flow graph, while the annotation // explains its missing interface. Preserve that distinction so subsequent // selections and applications both diagnose the restricting type once. @@ -2938,16 +3250,17 @@ class NewResolver: publish(UnknownValueShape(res, fromPublicInterface = false)(nominal.annotation.fold(provenance)(provenance.withTypeOrigin))) return case Marked(original: CallableTypeShape, context) => + if checked then res.callableTargets.currentShapes.add(source) val viewed = instantiateShape(original, instances) match case callable: CallableTypeShape => callable case _ => lastWords("A callable view must remain callable") val callable = instantiateCallable(viewed, res.resSym, lexicalBinders(res), context :: Nil) val ps = callable.paramLists.head - zipArgumentShapes(context :: Nil, ps.params.length, ps.hasRest, args, res, lhs): (index, value) => - (if index < ps.params.length then ps.params(index) else ps.rest).foreach: tpe => + if checked then zipArgumentShapes(context :: Nil, Arity(ps), args, res, lhs)((index, value) => + ps.allParams(index).tpe.foreach: tpe => value match - case value: TermShape => inferTypeArguments(tpe, value, context :: Nil) - case NoShape => () + case value: TermShape => inferTypeArguments(tpe, value, context :: Nil, S(res)) + case NoShape => ()) def publish(shape: TermShape)(using ResolutionContext): Unit = transportShape(shape, context :: Nil) match case value: TermShape => publishViewed(res, value, TypeSubstitution.empty)(using rctx.withInstances(callerInstances)) @@ -2962,12 +3275,15 @@ class NewResolver: lhs.unappliedParams match case Nil => () case (ps, mss) :: pss => - zipArgsIn(mss, ps.params, ps.restParam, args, res, lhs, instances) + if checked then zipArgsIn(mss, ps, args, res, lhs, instances) + else + // Protocol observations supply no arguments, so a rest parameter is empty. + ps.variadic.foreach(v => constrainParameterIn(v.param, + TupleShape(args, Nil)(this), mss, instances, N)) log(dbg"appShape isSaturated? ${sh.isSaturated}; head? ${sh.applicationHead._1.shwDbg}, ${sh.applicationHead._2.map(_.showDbg)}") def register(using ResolutionContext) = publish(sh) log(dbg"lhs ${lhs.isSaturated} ${lhs.unappliedParams.map(_.mapFirst(_.showDbg).mapSecond(_.map(_.showDbg)))}") - if lhs.isSaturated && !rctx.hasError(res) then - rctx.markError(res) + if lhs.isSaturated then // Marks transport a value across scopes; they do not apply arguments. // Inspect the value beneath them, retaining actual calls/instantiations // so an already constructed instance never gets the suggestion to use new. @@ -2978,7 +3294,17 @@ class NewResolver: msg"${lhs.describe.capitalize} cannot receive more argument lists." case _ => msg"${lhs.describe.capitalize} cannot be called like a function." - resolError(res, lhs.diagnostic(message)) + if checked then + // Conditional and recursive flow can lose the correlation that selects + // a callable branch. In lenient mode a known callable target suffices, + // just as a known member target does; never discard the other flows. + val exposed = lhs.applicationHead._1 match + case unknown: UnknownValueShape => unknown.fromPublicInterface + case _ => false + rctx.resolutionProblem(res, config.language.eagerResolution || exposed)(lhs.diagnostic(message)) + publish(UnknownValueShape.at(res)) + return + if checked then res.callableTargets.currentShapes.add(source) if sh.isSaturated then def go(body: Term, mss: Ls[Marks])(using ResolutionContext) = listenResult(body): sh => @@ -3084,7 +3410,7 @@ class NewResolver: msg"Cannot resolve member '$name' across a computed key or unknown record spread." host match case sel: NewSel => - rctx.missingMember(sel, config.language.eagerResolution || fromPublicInterface): + rctx.resolutionProblem(sel, config.language.eagerResolution || fromPublicInterface): provenance.diagnostic(message, N) case _ if !rctx.hasError(host) => rctx.markError(host) @@ -3098,6 +3424,7 @@ class NewResolver: ref.prefixes.foreach: prefix => listenReceiver(prefix): shape => def member(info: MemberLookup, instances: TypeSubstitution)(using ResolutionContext): Unit = info match + case MemberLookup.All(members) => members.foreach(member(_, instances)) case MemberLookup.Contextual(source, substitution) => member(source, instances.withOverrides(substitution)) case MemberLookup.Found(member, marks) if rctx.canResolve(ref) || ref.resolvedMembers.contains(prefix -> member.memberSymbol) => val candidate = prefix -> member.memberSymbol @@ -3226,6 +3553,7 @@ class NewResolver: */ def resolveSuper(sup: Super)(using ResolutionContext): Unit = def receive(info: MemberLookup, instances: TypeSubstitution)(using ResolutionContext): Unit = info match + case MemberLookup.All(members) => members.foreach(receive(_, instances)) case MemberLookup.Contextual(source, substitution) => receive(source, instances.withOverrides(substitution)) case MemberLookup.Found(member, marks) => publishMember(sup, member, sup.resSym, marks, instances) case MemberLookup.Declared(member, bindings, marks, annotation, positive) => @@ -3252,6 +3580,7 @@ class NewResolver: def visible(info: MemberLookup): MemberLookup = info.visibleFrom(scope.canAccessPrivatesOf, reportInaccessible = true) def member(info: MemberLookup, description: Message, diagnostic: Message => Ls[(Message, Opt[Loc])], instances: TypeSubstitution)(using ResolutionContext): Unit = info match + case MemberLookup.All(members) => members.foreach(member(_, description, diagnostic, instances)) case MemberLookup.Contextual(source, substitution) => member(source, description, diagnostic, instances.withOverrides(substitution)) case MemberLookup.Found(bms, marks) if rctx.canResolve(sel) || sel.resolvedMembers.contains(bms.memberSymbol) => log(dbg"newSel member: bms = ${bms.memberSymbol.showDbg}, mss = ${marks.map(_.showDbg)}") @@ -3277,7 +3606,7 @@ class NewResolver: rctx.recordResolution(sel, sel.hasDynamicTarget)(sel.hasDynamicTarget = true) publishDynamic(sel, marks) case MemberLookup.Missing if rctx.canResolve(sel) => - rctx.missingMember(sel, config.language.eagerResolution): + rctx.resolutionProblem(sel, config.language.eagerResolution): diagnostic(msg"$description does not contain member '${sel.id.name}'") case MemberLookup.Unknown(reason, fromPublicInterface, provenance) => unknownMember(sel, sel.id.name, reason, fromPublicInterface, provenance) @@ -3393,8 +3722,21 @@ class NewResolver: val sh = newShapes.getOrElseUpdate((ref.target, marks, nw.resSym, ref.supplied), NewShape(dsh, ref.target, marks, nw.args, nw, ref.supplied)) if rctx.state.constructorApplications.add((sh, instances)) then + // A class without parameter lists still accepts one empty argument tuple. + // Zipping alone would silently ignore that tuple, and any lists supplied + // after a constructor is saturated. Anonymous refinements forward these + // arguments through their synthetic parent New, which checks them there. + if nw.rft.isEmpty then + val paramCount = dsh.unappliedParams.length + val maxLists = paramCount.max(1) + if nw.args.length > maxLists then + resolError(nw, dsh.arityDiagnostic( + msg"Class '${cd.sym.nme}' expected at most ${maxLists} ${"argument list".pluralized(maxLists)}, but got ${nw.args.length}")) + if paramCount == 0 then + nw.args.headOption.foreach: args => + checkArgumentArity(args, Arity(0, 0, false, 0), nw, dsh)(_ => ()) dsh.unappliedParams.lazyZip(nw.args).foreach: - case ((ps, _), args) => zipArgsIn(marks, ps.params, ps.restParam, args, nw, dsh, instances) + case ((ps, _), args) => zipArgsIn(marks, ps, args, nw, dsh, instances) publishViewed(nw, sh, instances)(using rctx.withInstances(callerInstances)) ) , shape => @@ -3456,6 +3798,18 @@ class NewResolver: listenTerm(from): sh => publishActivated(to, sh) + /** The synthetic handler is an instance with real method definitions. Publish + * it after building those definitions, so forward selections made while + * elaborating the handled body resolve through ordinary member lookup. */ + def resolveAbortiveHandler(handle: Handle)(using ResolutionContext): Unit = + assert(handle.isAbortive) + val definition = handle.derivedClsSym.defn.get + listenExt(definition, parent => + val receiver = selfShapes.getOrElseUpdate(handle.derivedClsSym, BaseShape(definition, parent)) + MarkedShape.exit(receiver, ResolutionBoundary(handle.derivedClsSym), N) match + case value: TermShape => publishActivated(handle.lhs, value) + case NoShape => lastWords("A new handler instance must be reachable outside its class")) + // Classes without an explicit parent expose Object's declarations. Share this // root interface between inferred instances, nominal annotations, and this-values; // Object itself must terminate the chain. This does not add a runtime constructor call. @@ -3467,23 +3821,61 @@ class NewResolver: * definitions and follows inherited members using the same shapes as ordinary selection. */ def registerInheritance(defn: ClassLikeDef)(using ResolutionContext): Unit = - defn.body.members.values.toList.distinct.foreach: member => + // Distinct source aliases cannot define the same JavaScript property twice. + // Otherwise lookup could infer the first body while runtime keeps the last. + defn.body.methods.filterNot(_.tsym.isPrivate).flatMap(td => + MemberKey.symbol(td).map(_ -> td) + ).groupMap(_._1)(_._2).foreach: + case (key, definitions) if definitions.map(_.tsym).distinct.sizeCompare(1) > 0 => + defn.isErroneous = true + definitions.foreach(_.isErroneous = true) + rctx.report: + ErrorReport(msg"Duplicate definition of member '${key.name}'." -> N + :: definitions.map: td => + msg"Member '${td.tsym.nme}' is defined here." -> td.toLoc) + case _ => () + defn.body.members.valuesIterator.distinct.foreach: member => member.asTrm.filterNot(sym => sym.isInstanceOf[ClassCtorSymbol] || sym.isPrivate).foreach: own => // Bind this host to the defining unit before an importer can observe its edges. own.overrideLinks.inferenceHost + MemberKey.symbol(member).foreach: key => + listenInheritedMember(defn, key): lookup => + def check(info: MemberLookup): Unit = info match + case MemberLookup.All(members) => members.foreach(check) + case MemberLookup.Contextual(source, _) => check(source) + case MemberLookup.Found(base: BlockMemberSymbol, _) => collision(base) + case MemberLookup.Declared(base, _, _, _, _) => collision(base) + case _ => () + def collision(base: BlockMemberSymbol): Unit = if base.nme != member.nme then + defn.isErroneous = true + own.defn.foreach(_.isErroneous = true) + rctx.report(ErrorReport( + msg"Member '${member.nme}' uses the same @jsSymbol key as inherited member '${base.nme}'; overrides must keep the inherited name." -> own.toLoc :: + msg"The inherited member is declared here." -> base.toLoc :: Nil)) + check(lookup.visibleFrom(_ => false, reportInaccessible = false)) listenInheritedMember(defn, member.nme): lookup => def record(info: MemberLookup): Unit = info match + case MemberLookup.All(members) => members.foreach(record) case MemberLookup.Contextual(source, _) => record(source) case MemberLookup.Found(base: BlockMemberSymbol, _) => link(base) case MemberLookup.Declared(base, _, _, _, _) => link(base) case _ => () def link(base: BlockMemberSymbol): Unit = base.onComplete: () => base.asTrm.filterNot(_.isPrivate).foreach: target => - if target isnt own then own.overrideLinks.inferenceHost.publish(target) + if target isnt own then + if MemberKey.symbol(member) != MemberKey.symbol(base) then + // Source aliases must not silently redirect an override to a + // different runtime property. In particular, omitting jsSymbol + // on an override would leave the inherited protocol installed. + own.defn.foreach(_.isErroneous = true) + rctx.report(ErrorReport( + msg"Overriding member '${member.nme}' must use the same @jsSymbol annotation as its parent." -> own.toLoc :: + msg"The inherited member is declared here." -> target.toLoc :: Nil)) + own.overrideLinks.inferenceHost.publish(target) // Private members are not overridden, so they do not hide the inherited members they shadow. record(lookup.visibleFrom(_ => false, reportInaccessible = false)) - private def listenInheritedMember(defn: ClassLikeDef, name: Str) + private def listenInheritedMember(defn: ClassLikeDef, name: MemberKey) (listener: ShapeListener[MemberLookup])(using ResolutionContext): Unit = // Legacy preludes may have no Object declaration or even an enclosing prelude context. // Their root classes have no implicit nominal parent in the legacy resolver. @@ -3636,7 +4028,7 @@ class NewResolver: )(using rctx.withInstances(instances)) case S(d: TermDefinition) if !d.tsym.isInstanceOf[ClassCtorSymbol] && ((d.sign.nonEmpty && (!d.flags.hasResultAnnotation || d.params.forall(ps => - (ps.params ::: ps.restParam.toList).forall(_.sign.nonEmpty)))) || + ps.allParams.forall(_.sign.nonEmpty)))) || (d.body.isEmpty && d.tsym.owner.exists(_.asDefnSym.defn.exists(_.tparams.nonEmpty)))) => listenDeclaredMember(bms, instanceBindings(d), resSym, trm, N, selected, receiver, true, markss, specialization): value => transportShape(value, markss) match @@ -3686,11 +4078,26 @@ class NewResolver: * definitions. Keep validating selections/opens: a later distinct candidate * must be an error, since it cannot change an already interpreted annotation. */ + private def staticAnnotationSymbol(trm: Term): Opt[Symbol] = trm.withoutCaptures match + case ref: NewRefImpl => S(ref.sym) + case Ref(sym) => S(sym) + case sel: NewSel => staticAnnotationSymbol(sel.prefix).flatMap(_.asMod).flatMap(_.tree.definedSymbols.get(sel.id.name)) + case sel: Sel => staticAnnotationSymbol(sel.prefix).flatMap(_.asMod).flatMap(_.tree.definedSymbols.get(sel.nme.name)) + case _ => N + + /** Only member identity is needed to interpret an annotation. In particular a + * declared module has no runtime initializer to infer, and its syntax already + * identifies its members before their definitions are elaborated. Ordinary + * resolution still checks access and records targets for those selections. + */ def annotationSymbol(trm: Term)(using ResolutionContext): Opt[Symbol] = trm match case Capture(base, _) => annotationSymbol(base) case TyApp(base, _) => annotationSymbol(base) case App(base, _) => annotationSymbol(base) case ref: NewRefImpl => S(ref.sym) + case _ if staticAnnotationSymbol(trm).nonEmpty => + requireTerm(trm) + staticAnnotationSymbol(trm) case _: NewSel | _: UnresolvedRef => val symbols = mutable.LinkedHashSet.empty[BlockMemberSymbol] var collecting = true @@ -3717,6 +4124,71 @@ class NewResolver: resolError(trm, msg"An annotation must have a known main symbol." -> N :: Nil) N + /** Resolve the identity of a computed key, without checking its value's type. + * `fresh` is a user contract: distinct marked vals denote distinct keys. + * Modules and immutable vals make the path stable; calls and mutable bindings + * cannot be used to identify a property at compile time. A forward val's marker + * is checked when its definition completes, not by forcing its initializer. + */ + def symbolKey(term: Term, required: Bool)(using ResolutionContext): Opt[SymbolKey] = + def member(t: Term): Opt[Symbol] = staticAnnotationSymbol(t).orElse: + if required then annotationSymbol(t) + else + val members = mutable.LinkedHashSet.empty[BlockMemberSymbol] + listen(t, discardMarks = false): + case sh: SymShape => members += sh.sym + case _ => () + members.toList match + case sym :: Nil => S(sym) + case _ => N + def stable(sym: Symbol): Bool = sym.asMod.nonEmpty || (sym match + case bms: BlockMemberSymbol => bms.trmTree.exists(_.k is ImmutVal) || bms.asTrm.exists(_.k is ImmutVal) + case tsym: TermSymbol => tsym.k is ImmutVal + case _ => false) + def canonicalPath(path: Ls[Symbol], seen: Set[Symbol]): Ls[Symbol] = + path.foldLeft(Ls.empty[Symbol]): (prefix, sym) => + sym.asMod match + case S(module) => prefix :+ module + case N => + val base = sym.asTrm.flatMap(_.defn).map(_.sym).getOrElse(sym) + val alias = if seen(base) then N else base.asTrm.flatMap(_.defn).flatMap(_.body).flatMap(stablePath) + alias match + // An immutable alias replaces the whole receiver path, not just + // its last name: M.alias may refer to a val outside M. Only follow + // stable references; calls and other initializers remain opaque. + case S(target) => canonicalPath(target, seen + base) + case N => prefix :+ base + def stablePath(t: Term): Opt[Ls[Symbol]] = t.withoutCaptures match + case ref: NewRefImpl => member(t).filter(stable).map(_ :: Nil) + case Ref(sym) if stable(sym) => S(sym :: Nil) + case sel: NewSel => + for + prefix <- stablePath(sel.prefix) + sym <- member(sel).filter(stable) + yield prefix :+ sym + case sel: Sel => + for + prefix <- stablePath(sel.prefix) + sym <- member(sel).filter(stable) + yield prefix :+ sym + case _ => N + def fresh(sym: Symbol): Bool = sym.asTrm.flatMap(_.defn).exists(_.annotations.exists(_.isInstanceOf[Annot.Fresh])) + stablePath(term) match + case S(path) if path.last.asMod.isEmpty && (required || fresh(path.last)) => + if required then + def check(): Unit = if !fresh(path.last) then + resolError(term, msg"@jsSymbol requires a val marked @annotations.fresh." -> term.toLoc :: + msg"The referenced member is declared here." -> path.last.toLoc :: Nil) + path.last match + case member: BlockMemberSymbol => member.onComplete(() => check()) + case _ => check() + requireTerm(term) + S(SymbolKey(canonicalPath(path.init, Set.empty) :+ path.last.asTrm.flatMap(_.defn).map(_.sym).getOrElse(path.last))(term)) + case _ => + if required then resolError(term, + msg"@jsSymbol requires a stable path of modules and immutable vals ending in a val marked @annotations.fresh." -> term.toLoc :: Nil) + N + /** Values prefer the function/constructor overload; selection receivers prefer * the module overload. Keep that choice through captures so nested functions * and opened names use the same interpretation as direct references. @@ -3983,7 +4455,7 @@ class NewResolver: // fields into one trailing record. Shape positions must match that layout. case Fld(_, _, S(_)) :: rest => expand(rest, reversed) case (field: Fld) :: rest => expand(rest, TupleShape.Field(field, Nil) :: reversed) - case Spd(_, term) :: rest => + case Spd(_, term, _) :: rest => val spreadKey = new Object listenTermViews(term): sh => if rctx.state.spreadInputs.getOrElseUpdate(spreadKey, mutable.Set.empty).add(ShapeIdentity.key(sh)) then sh match @@ -4003,8 +4475,8 @@ class NewResolver: case Marked(_: DynShape, marks) => val spread = TupleShape(term, TupleShape.Unknown(term, TupleShape.ValueField(DynShape(), Nil)) :: Nil)(this) expand(rest, TupleShape.Spread(spread, marks) :: reversed) - case shape @ Marked(_, marks) => - // An array spread contributes an unknown number of elements, + case shape => + // An iterable spread contributes an unknown number of elements, // so its candidates would differ only in their element values. // Distinct element types (e.g. one annotation observed through // different activations) must not each become an operand: a @@ -4015,23 +4487,12 @@ class NewResolver: // one inferred element type, as for a tuple's Array interface. // Combinations of independent operands carry no correlation // between their element values, so no information is lost. - val elements = rctx.spreadElementType(term): - val host = new TypeResolution(term, messages => resolError(term, messages)) - // Elements of every operand shape are observed in the tuple's scope. - rctx.recordTemplateBinders(host, lexicalBinders(tuple)) - host - val typed = listenArrayElements(shape)(element => elements.publish(TypeShape.Inferred(element))) - if typed then - val element = TupleShape.TypedField(declaredType(elements, Map.empty), Nil) - val spread = TupleShape(term, TupleShape.Unknown(term, element) :: Nil)(this) - if rctx.state.spreadInputs.getOrElseUpdate(spreadKey, mutable.Set.empty).add(ShapeIdentity.key(spread)) then - expand(rest, TupleShape.Spread(spread, NoMarks) :: reversed) - else - // Other opaque iterables have no known element type. Their - // runtime spread remains permitted without authorizing calls. - val element = TupleShape.ValueField(UnknownValueShape.spread(term, shape), Nil) - val unknown = TupleShape(term, TupleShape.Unknown(term, element) :: Nil)(this) - expand(rest, TupleShape.Spread(unknown, marks) :: reversed) + val flow = iterationFlow(term, lexicalBinders(tuple)) + listenIterableElements(shape, flow) + val element = TupleShape.TypedField(declaredType(flow.elements, Map.empty), Nil) + val spread = TupleShape(term, TupleShape.Unknown(term, element) :: Nil)(this) + if rctx.state.spreadInputs.getOrElseUpdate(spreadKey, mutable.Set.empty).add(ShapeIdentity.key(spread)) then + expand(rest, TupleShape.Spread(spread, NoMarks) :: reversed) expand(elements, Nil) case record: Rcd => listenAggregate(record, listener): publish => def expand(stats: Ls[Statement], reversed: Ls[RecordShape.Element])(using ResolutionContext): Unit = stats match @@ -4054,6 +4515,9 @@ class NewResolver: case _ => expand(rest, RecordShape.Unknown(UnknownValueShape.spread(term, shape)) :: reversed) case _ :: rest => expand(rest, reversed) expand(record.stats, Nil) + // Ignore undefined during resolution. Runtime literals and pattern tests + // are compiled from the term itself and do not need a shape. + case Lit(Tree.UnitLit(false)) => () case intro: IntroTerm => // Literals and lambdas have the declared interface of their builtin class, // including inherited members. Retain their own shape for calls, pattern @@ -4144,9 +4608,13 @@ class NewResolver: // Finally is evaluated for effects; normal completion returns the body's // value. Its cleanup value must not contribute candidates to this result. if resultFlow(cleanup).normal then listen(body, discardMarks)(listener) - case Label(label, _, body, _) => + case Label(label, _, body, hasContinueDispatch) => val flow = resultFlow(body) - if flow.normal then listen(body, discardMarks)(listener) + if flow.normal then listen(body, discardMarks): + // Lowering uses runtime.Continue to re-enter the label. Only normal + // results and non-local break values flow out of the label. + case value: TermShape if hasContinueDispatch && value.isInstanceOfClass(State.continueSymbol.defn.get) => () + case value => listener(value) flow.exits.getOrElse(S(label), Vector.empty).foreach(value => listen(value, discardMarks)(listener)) case annotated @ Annotated(annotation @ Annot.Async(), body) => listener(promiseShape(annotation, resultType(body, lexicalBinders(annotated), rctx.instances)(listenResult(body)))) @@ -4157,6 +4625,18 @@ class NewResolver: // Quotation constructs a runtime syntax value; splicing supplies syntax // for a different stage. Neither has the value interface of its operand. listener(UnknownValueShape.at(trm)) + case handle: Handle if handle.isAbortive => + resolveAbortiveHandler(handle) + listenResult(handle.body)(listener) + handle.defs.foreach: method => + // Parameters arrive through the method and handler-instance boundaries. + // The result leaves both boundaries, joining every invocation at the + // handler site rather than returning to the abandoned continuation. + listenDefinitionResult(method.td, rctx.instances): result => + result.exit(ExitMark(ResolutionBoundary(method.td.tsym), N, NoMarks)) + .exit(ExitMark(ResolutionBoundary(handle.derivedClsSym), N, NoMarks)) match + case value: TermShape => listener(value) + case NoShape => () case _: Handle => // An effect handler can abort or replace the continuation's result. The // normal body's interface alone therefore cannot describe this value. diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolverState.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolverState.scala index 2202c3eb2d..26152f71a0 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolverState.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/NewResolverState.scala @@ -79,8 +79,8 @@ final class NewResolverState(val owner: Elaborator.State): private[semantics] val copies = mutable.Map.empty[Publisher.Data[?], Publisher.Data[?]] private[semantics] val pending = mutable.Map.empty[Identity[Publisher[?]], Publisher.Data[?]] private[semantics] val completedNodes = mutable.Set.empty[Identity[Publisher[?]]] - private[semantics] val missingMembers = mutable.LinkedHashMap.empty[Identity[NewSel], - mutable.ListBuffer[(Bool, () => Ls[(Message, Opt[Loc])])]] + private[semantics] val resolutionProblems = + mutable.LinkedHashMap.empty[Identity[NewSel | App], mutable.ListBuffer[() => Ls[(Message, Opt[Loc])]]] // The references, selections, and instantiations written in this unit's current block, // with the lexical scope in which each is written (see ResolutionContext.checkAccess). private[semantics] val accessSites = mutable.ArrayBuffer.empty[(NewResolvable | New, SrcScope)] @@ -88,7 +88,7 @@ final class NewResolverState(val owner: Elaborator.State): private[semantics] val shapeIdentities = mutable.HashMap.empty[ShapeIdentity.View, ShapeIdentity.View] private[semantics] val substitutions = mutable.HashMap.empty[TypeSubstitution, TypeSubstitution] private[semantics] val typeReferences = new DeclaredType.References(substitutions) - val membersCache: Cache[(TermShape, Str), MemberLookup] = + val membersCache: Cache[(TermShape, MemberKey), MemberLookup] = new Cache(_.membersCache, identity) // Shape components of cache keys use ShapeIdentity.key, like hosts' candidates. val spreadInputs: Cache[Object, mutable.Set[ShapeIdentity.Key]] = @@ -116,8 +116,8 @@ final class NewResolverState(val owner: Elaborator.State): // Adopted canonical producers are not newly allocated (see canonical). private[semantics] var allocatedShapeViews: Int = 0 private[semantics] var largestShapeViewSubstitution: Int = 0 - // The inferred element type of each array spread (see the tuple listener). - private[semantics] val spreadElements: Cache[Identity[Term], TypeResolution] = + // Stable iteration observations and joined elements for each spread operand. + private[semantics] val spreadElements: Cache[Identity[Term], IterationFlow] = new Cache(_.spreadElements, identity) val introShapes: Cache[Identity[IntroTerm], IntroShape] = new Cache(_.introShapes, identity) @@ -144,8 +144,6 @@ final class NewResolverState(val owner: Elaborator.State): // candidates. Register each host before following recursive function results. val resultTypes: Cache[(Identity[Term], TypeSubstitution), DeclaredType] = new Cache(_.resultTypes, identity) - val awaitedTypes: Cache[DeclaredType, DeclaredType] = - new Cache(_.awaitedTypes, identity) // Keyed like inference candidates (see ShapeIdentity), so each candidate has one awaited type. val awaitedValues: Cache[(ShapeIdentity.Key, TypeSubstitution), DeclaredType] = new Cache(_.awaitedValues, identity) @@ -248,7 +246,7 @@ final class NewResolverState(val owner: Elaborator.State): new Cache(_.patternRefinements, identity) val primitiveTypes: Cache[ClassSymbol, DeclaredType] = new Cache(_.primitiveTypes, identity) - val extremeTypes: Cache[(Bool, Opt[TypeResolution]), DeclaredType] = + val extremeTypes: Cache[Bool, DeclaredType] = new Cache(_.extremeTypes, identity) val variantTypes: Cache[(DeclaredType, Bool), DeclaredType] = new Cache(_.variantTypes, identity) @@ -262,6 +260,9 @@ final class NewResolverState(val owner: Elaborator.State): new Seen(_.exposedTypeHoles) val signatureParameters: Cache[VarSymbol, DeclaredType] = new Cache(_.signatureParameters, identity) + // Synthesized applications share their type graph, including the symbolic arguments. + val appliedTypes: Cache[(ClassSymbol | TypeAliasSymbol, Ls[DeclaredType]), DeclaredType] = + new Cache(_.appliedTypes, identity) val contextualTypes: Cache[ContextualType, DeclaredType] = new Cache(_.contextualTypes, identity) val tupleArrayParents: Cache[Identity[TupleShape], NominalInstanceView] = @@ -386,20 +387,25 @@ final class ResolutionContext private[semantics] ( /** Number of lazily copied graph nodes, for isolation/scale regressions. */ private[hkmc2] def copiedHostCount: Int = state.copies.size - // A receiver shape can lack a member that another shape supplies later in - // inference. Retain lazy witnesses by selection, including those from later - // activations of completed syntax, and report them together at block completion. - // Eager problems invalidate the selection immediately, regardless of its targets; + // A candidate can lack a member or be non-callable while another candidate + // supplies a target later in inference. Retain lazy witnesses by use site, + // including later activations of completed syntax, and report them together + // at block completion. Eager problems invalidate the use immediately; // delaying their report lets the same error include every relevant witness. - private def hasSelectionTarget(selection: NewSel): Bool = - selection.resolvedTargets.nonEmpty || selection.hasDynamicTarget || selection.tupleIndex.nonEmpty - def missingMember(selection: NewSel, eager: Bool)(messages: => Ls[(Message, Opt[Loc])]): Unit = - val key = new Identity(selection) + def resolutionProblem(term: NewSel | App, eager: Bool)(messages: => Ls[(Message, Opt[Loc])]): Unit = + val key = new Identity[NewSel | App](term) // Accumulate witnesses for this block's error without replaying errors on - // completed selections or adding follow-on errors to unrelated failures. - if !hasError(selection) || state.missingMembers.contains(key) then - if eager then markError(selection) - state.missingMembers.getOrElseUpdate(key, mutable.ListBuffer.empty) += ((eager, () => messages)) + // completed terms or adding follow-on errors to unrelated failures. + if !hasError(term) || state.resolutionProblems.contains(key) then + if eager then markError(term) + state.resolutionProblems.getOrElseUpdate(key, mutable.ListBuffer.empty) += (() => + // Keep this observation's graph: an intermediate exporter can have + // callable witnesses in its private copy of a third file's application. + val unresolved = term match + case selection: NewSel => + selection.resolvedTargets.isEmpty && !selection.hasDynamicTarget && selection.tupleIndex.isEmpty + case application: App => application.callableTargets.currentShapes(using this).isEmpty + if eager || unresolved then messages else Nil) /** Seal the decisions and original host data read by erasure/lowering. The * inference graph keeps private, live host data and all its listeners, so @@ -432,6 +438,7 @@ final class ResolutionContext private[semantics] ( case term: Term => data(term) case _ => () statement match + case application: App => data(application.callableTargets) case cls: ClassLikeDef => cls.ext.foreach(visit) cls.auxParams.foreach(_.subTerms.foreach(visit)) @@ -448,17 +455,21 @@ final class ResolutionContext private[semantics] ( case _ => () statement.subStatements.foreach(visit) visit(block) - state.missingMembers.foreach: (key, problems) => - val selection = key.value - val unresolved = !hasSelectionTarget(selection) - val messages = problems.iterator.collect: - case (eager, messages) if eager || unresolved => messages() - .flatten.toList.distinct + state.resolutionProblems.foreach: (key, problems) => + val term = key.value + val messages = problems.iterator.flatMap(_()).toList.distinct if messages.nonEmpty then - markError(selection) - reportResolutionError(selection, messages) - state.missingMembers.clear() - state.accessSites.foreach(checkAccess) + markError(term) + reportResolutionError(term, messages) + state.resolutionProblems.clear() + state.accessSites.foreach: (term, scope) => + checkAccess(term, scope) + // Access checks can insert already-resolved key expressions. Bind their + // hosts to this unit before sealing, just like the written source terms. + term match + case ref: NewResolvable => ref.keyAccess.foreach(visit) + case nw: New if nw.args.isEmpty && nw.rft.isEmpty => checkPartialInstantiation(nw) + case _ => () state.accessSites.clear() state.pending.foreach: (key, value) => if !key.value.isInstanceOf[Symbol] && state.completedNodes.add(key) then value.completed = true @@ -471,6 +482,23 @@ final class ResolutionContext private[semantics] ( def recordAccessSite(term: NewResolvable | New, scope: SrcScope): Unit = state.accessSites += ((term, scope)) + /** Check written instantiations once their aliases and forward references have + * resolved. Using the per-block source sites avoids repeating the warning when + * generic functions are instantiated or their inference is imported elsewhere. */ + private def checkPartialInstantiation(term: New): Unit = if !hasError(term) then + ClassValueShape.targetsOf(term).map(ClassValueShape.definitionOf).distinct match + case cls :: Nil => + (cls.paramsOpt.toList ::: cls.auxParams) match + case params :: Nil if params.paramCountLB == 0 && !params.flags.ctx => + val construction = term.toLoc.fold(s"new ${cls.sym.nme}"): loc => + loc.origin.fph.blockStr.slice(loc.spanStart, loc.spanEnd) + report(WarningReport( + msg"This 'new' expression partially applies class '${cls.sym.nme}' instead of constructing an instance." -> term.toLoc :: + msg"Use `$construction()` to construct an instance, or `=> $construction()` to create an explicit zero-argument lambda." -> N :: + msg"The constructor accepts an empty argument list." -> params.toLoc :: Nil)) + case _ => () + case _ => () + /** Private members are only accessible from within their owner and its companion (see * SrcScope.canAccessPrivatesOf); member lookups performed elsewhere skip them in favor of * inherited members (see MemberLookup.visibleFrom), so a reference written there only @@ -486,7 +514,7 @@ final class ResolutionContext private[semantics] ( */ private def checkAccess(term: NewResolvable | New, scope: SrcScope): Unit = if !hasError(term) then val loc = term match - case ref: MemberRef => ref.tree.toLoc + case ref: MemberRef => ref.toLoc case sel: NewSel => sel.id.toLoc case ref: UnresolvedRef => ref.id.toLoc case sup: Super => sup.id.toLoc @@ -556,6 +584,36 @@ final class ResolutionContext private[semantics] ( if violations.nonEmpty then markError(term) violations.foreach(report) + else term match + case ref: NewResolvable => + // Overrides can have several targets but still share one property key. + // Keep those targets and their flows; recover only their common operand. + ref.resolvedTargets.map(_.asTrm.flatMap(_.defn).flatMap(MemberKey.symbol)).distinct match + case S(key) :: Nil => + AccessPath.symbolKey(scope, key)(using state.owner) match + case R(path) => ref.keyAccess = S(path) + case L(AccessPath.Failure.PrivateMember(member)) => + markError(ref) + report(ErrorReport( + msg"Cannot use @jsSymbol key '${key.name}' here: private member '${member.nme}' belongs to another file." -> loc :: + msg"The private member is declared here." -> member.toLoc :: + ref.resolvedTargets.distinct.map(target => + msg"The keyed member '${target.nme}' is defined here." -> target.toLoc), + source = Diagnostic.Source.Compilation)) + case L(AccessPath.Failure.OutOfScope) => + markError(ref) + report(ErrorReport( + msg"Cannot use @jsSymbol key '${key.name}' here: '${key.path.head.nme}' is not in scope." -> loc :: + msg"The key's root binding is defined here." -> key.path.head.toLoc :: Nil, + source = Diagnostic.Source.Compilation)) + case keys if keys.exists(_.nonEmpty) => + markError(ref) + report(ErrorReport( + msg"This reference has targets with different property keys." -> loc :: + ref.resolvedTargets.distinct.map(target => msg"Candidate '${target.nme}' is declared here." -> target.toLoc), + source = Diagnostic.Source.Compilation)) + case _ => () + case _: New => () def canResolve(host: Publisher[?]): Bool = data(host) @@ -603,7 +661,7 @@ final class ResolutionContext private[semantics] ( canonical(key._1, _.recordShapes, key)(shapeView(key._3)(make)) def canonicalInstance(tpe: DeclaredType)(make: => InstanceShape): InstanceShape = canonical(tpe.resolution, _.instanceShapes, tpe)(shapeView(tpe.instances)(make)) - def spreadElementType(spread: Term)(make: => TypeResolution): TypeResolution = + def spreadFlow(spread: Term)(make: => IterationFlow): IterationFlow = canonical(spread, _.spreadElements, new Identity(spread))(make) def memberVariable(symbol: TermSymbol): ShapeHost = state.memberVariables.getOrElseUpdate(symbol, @@ -706,6 +764,14 @@ final class ResolutionContext private[semantics] ( def hasTypeConstraint(tpe: DeclaredType, value: TermShape, marks: Ls[Marks]): Bool = typeConstraints((tpe, ShapeIdentity.key(value), marks)) +/** One pair of call sites per spread source. Incoming alternatives share these + * observations and the element host, so their Cartesian product is never built. + * Publisher storage and listeners remain owned by the consuming resolution graph. + */ +private[semantics] final class IterationFlow( + val elements: TypeResolution, val iterator: Term.App, val next: Term.App, + val iteratorMember: FlowSymbol, val nextMember: FlowSymbol, val doneMember: FlowSymbol, val valueMember: FlowSymbol) + private[semantics] final class TermShapeHost extends Host[TermShape]: def showDbg(using DebugPrinter): Str = "instance views" def publish(shape: TermShape)(using ResolutionContext): Unit = diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ParameterOrder.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ParameterOrder.scala new file mode 100644 index 0000000000..c8248a445b --- /dev/null +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ParameterOrder.scala @@ -0,0 +1,28 @@ +package hkmc2 +package semantics + +import hkmc2.utils.*, shorthands.* +import hkmc2.Message.MessageContext + +/** Ordering rules shared by value parameters and function type parameters. + * Callers keep their own binding and recovery logic: even an invalid parameter + * advances the order, so later parameters are checked against the written list. + * Restrictions specific to declarations (such as contextual parameters) are + * checked before asking this validator about an optional parameter's position. + */ +private[semantics] final case class ParameterOrder(seenOptional: Bool, seenVariadic: Bool): + def checkFixed(optional: Bool, loc: Opt[Loc])(using Raise): Unit = + if optional && seenVariadic then + raise(ErrorReport(msg"Optional parameters cannot follow a variadic parameter." -> loc :: Nil)) + else if !optional && seenOptional && !seenVariadic then + raise(ErrorReport(msg"A required parameter cannot follow an optional parameter." -> loc :: Nil)) + + def checkVariadic(loc: Opt[Loc])(using Raise): Unit = + if seenVariadic then + raise(ErrorReport(msg"A parameter list cannot have more than one variadic parameter." -> loc :: Nil)) + + def next(optional: Bool, variadic: Bool): ParameterOrder = + ParameterOrder(seenOptional || optional, seenVariadic || variadic) + +private[semantics] object ParameterOrder: + val empty: ParameterOrder = ParameterOrder(false, false) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala index ddc1fe3b8f..aec5f72caf 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Resolver.scala @@ -660,11 +660,12 @@ class Resolver(tl: TraceLogger) case _ => (false, 0) (ps.paramCountUB, argCountUB) match - case (true, true) => if ps.paramCountLB != argCountLB then - raise(ErrorReport(msg"Expected ${ps.paramCountLB.toString()} arguments, " + + // * The legacy resolver does not describe optional parameters in its messages. + case (true, true) => if ps.paramCountLB > argCountLB || ps.paramCountMax < argCountLB then + raise(ErrorReport(msg"Expected ${ps.paramCountMax.toString()} arguments, " + msg"got ${argCountLB.toString()}" -> as.toLoc :: Nil)) - case (true, false) => if ps.paramCountLB < argCountLB then - raise(ErrorReport(msg"Expected ${ps.paramCountLB.toString()} arguments, " + + case (true, false) => if ps.paramCountMax < argCountLB then + raise(ErrorReport(msg"Expected ${ps.paramCountMax.toString()} arguments, " + msg"got at least ${argCountLB.toString()}" -> as.toLoc :: Nil)) case (false, true) => if ps.paramCountLB > argCountLB then raise(ErrorReport(msg"Expected at least ${ps.paramCountLB.toString()} arguments, " + @@ -720,7 +721,7 @@ class Resolver(tl: TraceLogger) val args = as match case Term.Tup(args) => args case Term.CtxTup(args) => args - case spd => Spd(SpreadKind.Eager, spd) :: Nil + case spd => Spd(SpreadKind.Eager, spd, false) :: Nil // The lhs of the App is already traversed by the recursive // `traverse` or `resolve` at the beginning. @@ -752,7 +753,10 @@ class Resolver(tl: TraceLogger) case (ps @ ParamList( flags = ParamListFlags(ctx = false) )) :: pss if !inAppPrefix && pss.exists(_.flags.ctx) => - val as = ps.params.map(p => Fld(p.flags, p.sym.ref().dontResolve, N)) + def arg(p: Param) = Fld(p.flags, p.sym.ref().dontResolve, N) + // The variadic parameter is forwarded as a spread, followed by its trailing parameters. + val as = ps.params.map(arg) ::: ps.variadic.toList.flatMap: v => + Spd(SpreadKind.Eager, v.param.sym.ref().dontResolve, false) :: v.trailing.map(arg) // val as = ps.params.map(p => Fld(p.flags, p.sym.ref().resolve, N)) val newLam = (t: Term) => lam(Term.Lam(ps, t)) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala index 87b217cd29..92c125ea3b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Shape.scala @@ -29,16 +29,18 @@ sealed trait Shape extends ShapeEvent: * from the observed value to that witness. Wrappers preserve this ordering. * An annotation restricting a value and its class declaration are different * witnesses: interface errors use the annotation, while toLoc identifies the - * declaration. Diagnostic witnesses do not change semantic keys. + * declaration. Unknown values cite their provenance; the enclosing resolution + * error already locates the operation, so repeating its operand adds no evidence. + * Diagnostic witnesses do not change semantic keys. */ final def diagnostic(message: Message): Ls[(Message, Opt[Loc])] = this match case value: TermShape => value.applicationHead._1 match case view: NominalInstanceView => view.provenance.diagnostic(message, view.annotation.flatMap(_.toLoc).orElse(toLoc)) case view: ContextualShape => view.source.diagnostic(message) case view: SpecializedShape => view.declaration.diagnostic(message) - case unknown: UnknownValueShape => unknown.provenance.diagnostic(message, toLoc) + case unknown: UnknownValueShape => unknown.provenance.diagnostic(message, N) case opaque: OpaqueTypeShape => opaque.provenance.diagnostic(message, toLoc) - case rigid: RigidTypeShape => rigid.provenance.diagnostic(message, toLoc) + case rigid: RigidTypeShape => rigid.provenance.diagnostic(message, N) case _ => (message -> toLoc) :: Nil case _: SymShape => (message -> toLoc) :: Nil /** Arity errors also cite where the expected parameter lists are declared, when the @@ -65,6 +67,7 @@ sealed trait Shape extends ShapeEvent: case ts: TupleShape => s"TupleShape(${ts.source.showDbg})" case rs: RecordShape => s"RecordShape(${rs.source.showDbg})" case rs: RecordTypeShape => s"RecordTypeShape(${rs.source.showDbg})" + case view: IntersectionView => s"IntersectionView(${view.values.map(_.shwDbg).mkString(", ")})" case ts: InstanceShape => s"InstanceShape(${ts.tpe.resolution.source.showDbg})" case ts: NominalInstanceView => s"NominalInstanceView(${ts.defn.sym.showDbg})" case ts: OpaqueTypeShape => s"OpaqueTypeShape(${ts.source.showDbg})" @@ -90,7 +93,7 @@ object ShapeIdentity: private[semantics] def comparison(shape: ShapeEvent): Key = shape match case instance: InstanceShape => instance.candidateIdentity case _: (MarkedShape[?] | ContextualShape | ClassValueShape | NominalInstanceView | - CallableTypeShape | RecordTypeShape | SpecializedShape | UnknownValueShape | + CallableTypeShape | RecordTypeShape | IntersectionView | SpecializedShape | UnknownValueShape | OpaqueTypeShape | RigidTypeShape | ActivatedShapeEvent) => new View(shape) // These producers own graph nodes, including synthesized inference hosts. // Adding a variant requires choosing its equality policy here. @@ -111,7 +114,9 @@ object ShapeIdentity: 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) - case view: RecordTypeShape => ValueKey.mapHash(view.fields)(_.hashCode, _.hashCode) + case view: RecordTypeShape => ValueKey.hash( + ValueKey.mapHash(view.fields)(_.hashCode, _.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) case view: UnknownValueShape => ValueKey.hash(System.identityHashCode(view.source), view.fromPublicInterface.hashCode) @@ -139,7 +144,10 @@ object ShapeIdentity: a.declaration.fold(b.declaration.isEmpty)(x => b.declaration.exists(_ eq x)) && a.asyncResult == b.asyncResult case _ => false case a: RecordTypeShape => that.shape match - case b: RecordTypeShape => a.fields == b.fields + case b: RecordTypeShape => a.fields == b.fields && a.aliases == b.aliases + case _ => false + case a: IntersectionView => that.shape match + case b: IntersectionView => a.values.map(_.comparison) == b.values.map(_.comparison) case _ => false case a: SpecializedShape => that.shape match case b: SpecializedShape => a.declaration.comparison == b.declaration.comparison && @@ -302,7 +310,7 @@ object Marked: end Marked final case class MarkedShape[+T <: NonMarkedShape](sh: T, mark: SomeMarks) extends TermShape: - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = sh.getMemberThrough(name, mark) override def isInstanceOfClass(cls: ClassLikeDef)(using ResolutionContext): Bool = sh.isInstanceOfClass(cls) def describe: Str = sh.describe @@ -340,15 +348,15 @@ end MarkedShape sealed trait TermShape extends Shape, ShapeLike: // Cache all lookup outcomes per shape, without materializing all inherited members. - final def getMember(name: Str)(using state: ResolutionContext): MemberLookup = + final def getMember(name: MemberKey)(using state: ResolutionContext): MemberLookup = state.state.membersCache.getOrElseUpdate((this, name), getMemberImpl(name)) - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup /** Lookup on this shape as reached through the receiver path `receiver`. * Shapes whose members are declared by a nominal class override this to * locate the member at the class's definition (see NewResolver.nominalMember); * other members are transported through the whole path. */ - def getMemberThrough(name: Str, receiver: Marks)(using ResolutionContext): MemberLookup = receiver match + def getMemberThrough(name: MemberKey, receiver: Marks)(using ResolutionContext): MemberLookup = receiver match case NoMarks => getMember(name) case receiver: SomeMarks => getMember(name).withMarks(receiver :: Nil) @@ -445,6 +453,9 @@ end TermShape /** Missing means definitely absent; Unknown must not be treated as a miss when * searching wildcard opens, since it can introduce an additional candidate. */ enum MemberLookup: + // A structural intersection can give one source name several runtime keys. + // Retain each target so selection reports ambiguity instead of choosing one. + case All(members: Ls[MemberLookup]) case Contextual(source: MemberLookup, instances: TypeSubstitution) case Found(member: BlockMemberSymbol | RecordMember, marks: Ls[Marks]) // Declared members expose signatures only. Their marks transport dependent @@ -463,6 +474,7 @@ enum MemberLookup: case Private(own: MemberLookup, owner: ClassLikeDef, inherited: MemberLookup) def withMarks(marks: Ls[Marks]): MemberLookup = this match + case All(members) => All(members.map(_.withMarks(marks))) case Contextual(source, instances) => Contextual(source.withMarks(marks), instances) case Private(own, owner, inherited) => Private(own.withMarks(marks), owner, inherited.withMarks(marks)) case Found(member, inner) => Found(member, inner ::: marks) @@ -474,6 +486,7 @@ enum MemberLookup: /** The receiver's annotation remains the diagnostic origin when lookup visits a parent. */ def withAnnotation(annotation: Opt[Term]): MemberLookup = this match + case All(members) => All(members.map(_.withAnnotation(annotation))) case Contextual(source, instances) => Contextual(source.withAnnotation(annotation), instances) case Private(own, owner, inherited) => Private(own.withAnnotation(annotation), owner, inherited.withAnnotation(annotation)) case Declared(member, bindings, marks, _, positive) => Declared(member, bindings, marks, annotation, positive) @@ -493,6 +506,7 @@ enum MemberLookup: * rather than a missing member; otherwise, the lookup finds no member. */ def visibleFrom(canAccess: ClassLikeDef => Bool, reportInaccessible: Bool): MemberLookup = this match + case All(members) => MemberLookup.all(members.map(_.visibleFrom(canAccess, reportInaccessible))) case Contextual(source, instances) => source.visibleFrom(canAccess, reportInaccessible).instantiate(instances) case Private(own, owner, inherited) => if canAccess(owner) then own @@ -502,14 +516,19 @@ enum MemberLookup: case _ => this object MemberLookup: + def all(members: Ls[MemberLookup]): MemberLookup = members.filterNot(_ == Missing) match + case Nil => Missing + case member :: Nil => member + case members => All(members) + enum Uncertainty: case ValueShape, RecordOverwrite /** Own members take precedence over inherited ones, including unknown spreads, * except for private members as seen from outside their class (see Private). */ - def inClass(defn: ClassLikeDef, ext: Opt[TermShape], name: Str)(using ResolutionContext): MemberLookup = + def inClass(defn: ClassLikeDef, ext: Opt[TermShape], name: MemberKey)(using ResolutionContext): MemberLookup = def inherited = ext.fold[MemberLookup](Missing)(_.getMember(name)) - defn.body.members.get(name) match + MemberKey.own(defn, name) match case S(member) => own(defn, member, Found(member, Nil), inherited) case N => inherited @@ -531,11 +550,11 @@ extension (member: BlockMemberSymbol | RecordMember) class ErrShape(val err: ErrorReport) extends CoreHeadShape: override def isSelfContained: Bool = true def describe: Str = s"error: ${err.mainMsg}" - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = MemberLookup.Missing + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = MemberLookup.Missing def toLoc: Opt[Loc] = N class AppShape(val receiver: CoreTermShape, val args: Term, val src: Term.App)(using DebugPrinter) extends CoreShape: - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = // An unsaturated term definition is just a concrete function shape if !isSaturated then MemberLookup.Missing else @@ -561,7 +580,7 @@ class AppShape(val receiver: CoreTermShape, val args: Term, val src: Term.App)(u class NewShape(val receiver: DefnShape, val classTarget: ClassValueTarget, val clsMarks: Ls[Marks], val argss: Ls[Term], val src: Term.New, val supplied: Opt[Ls[DeclaredType]])(using DebugPrinter) extends CoreShape: def cls: ClassSymbol = ClassValueShape.definitionOf(classTarget).sym - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = if isSaturated then receiver.getInstanceMember(name).withMarks(clsMarks) else MemberLookup.Missing override def isInstanceOfClass(cls: ClassLikeDef)(using ResolutionContext): Bool = @@ -631,7 +650,7 @@ class BaseShape(val defn: ClassLikeDef, val ext: Opt[TermShape]) extends CoreHea override def isInstanceOfClass(cls: ClassLikeDef)(using ResolutionContext): Bool = (defn is cls) || ext.exists(_.isInstanceOfClass(cls)) def describe: Str = s"${defn.describe}" - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = MemberLookup.inClass(defn, ext, name) def toLoc: Opt[Loc] = defn.headerLoc @@ -641,14 +660,30 @@ class BaseShape(val defn: ClassLikeDef, val ext: Opt[TermShape]) extends CoreHea * Source declarations are witnesses for diagnostics and property selection, not * part of semantic equality: a merged field may have several such witnesses. */ -final class RecordTypeShape(val fields: Map[Str, DeclaredType])( - val source: Term.Rcd, val declarations: Map[Str, RcdField]) extends CoreHeadShape: +final class RecordTypeShape(val fields: Map[MemberKey, DeclaredType], 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)" def toLoc: Opt[Loc] = source.toLoc - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = - fields.get(name).fold[MemberLookup](MemberLookup.Missing)(tpe => - MemberLookup.Typed(declarations(name), tpe, Nil)) + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = + // Aliases affect lookup and therefore semantic identity, independently of + // diagnostic declarations. Intersections retain all aliases for a shared key. + val keys = Option.when(fields.contains(name))(name).toList ::: (name match + case name: Str => aliases.getOrElse(name, Set.empty).toList.sortBy(_.name) + case _ => Nil) + MemberLookup.all(keys.map(key => MemberLookup.Typed(declarations(key), fields(key), Nil))) + +/** A conjunction exposes members supplied by any component. Keep nominal and + * structural interfaces together: `Iterator[T] & Iterable[T]` supplies `next` + * and the iteration method on the same value. Constraint inspection still + * visits the components separately, retaining every input and output flow. + */ +final class IntersectionView(val values: Ls[TermShape]) extends CoreHeadShape: + def describe: Str = "intersection type" + def toLoc: Opt[Loc] = values.headOption.flatMap(_.toLoc) + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = + MemberLookup.all(values.map(_.getMember(name))) + override def isInstanceOfClass(cls: ClassLikeDef)(using ResolutionContext): Bool = values.exists(_.isInstanceOfClass(cls)) /** An instance described by a type, in either position of a constraint. Keep the * type reference intact during transport: expanding it to its current positive @@ -661,7 +696,7 @@ final class InstanceShape private (val tpe: DeclaredType, canonical: Opt[Instanc def describe: Str = "value with a declared type" override def toString: Str = s"InstanceShape($tpe)" def toLoc: Opt[Loc] = tpe.resolution.source.toLoc - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = lastWords("Instance member lookup requires interpreting its type first") object InstanceShape: @@ -698,8 +733,8 @@ final case class ContextualShape(source: ContextualSource, instances: TypeSubsti def describe: Str = source.describe def toLoc: Opt[Loc] = source.toLoc override def isInstanceOfClass(cls: ClassLikeDef)(using ResolutionContext): Bool = source.isInstanceOfClass(cls) - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = source.getMember(name).instantiate(instances) - override def getMemberThrough(name: Str, receiver: Marks)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = source.getMember(name).instantiate(instances) + override def getMemberThrough(name: MemberKey, receiver: Marks)(using ResolutionContext): MemberLookup = source.getMemberThrough(name, receiver).instantiate(instances) /** Explicit type application, such as f[Int], has consumed this declaration's @@ -714,7 +749,7 @@ final case class SpecializedShape(declaration: DefnShape, arguments: Ls[Declared instances: TypeSubstitution) extends NonAppTermShape: def describe: Str = declaration.describe def toLoc: Opt[Loc] = declaration.toLoc - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = declaration.getMember(name).instantiate(instances) + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = declaration.getMember(name).instantiate(instances) /** An inference-event envelope, not a value interface. `publishActivated` saves * the current body's original-binder-to-instance map; `listen` checks agreement @@ -750,21 +785,37 @@ final class NominalInstanceView(val defn: ClassLikeDef, val bindings: Map[VarSym else parent.flatMap: case Marked(parent: NominalInstanceView, _) => parent.ancestor(cls) case _ => N - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = resolver.nominalMember(this, name, NoMarks) - override def getMemberThrough(name: Str, receiver: Marks)(using ResolutionContext): MemberLookup = + override def getMemberThrough(name: MemberKey, receiver: Marks)(using ResolutionContext): MemberLookup = resolver.nominalMember(this, name, receiver) +/** One declared input, keeping its optional/droppable flags with its type through + * substitution and layout matching. An absent type still occupies a parameter position. + */ +final case class DeclaredParam(tpe: Opt[DeclaredType], flags: FldFlags): + def mapType(f: DeclaredType => DeclaredType): DeclaredParam = copy(tpe = tpe.map(f)) + /** Parameter types and arity exposed by one list in a declared calling interface. - * A rest annotation describes the whole trailing array; hasRest also distinguishes - * an unannotated rest parameter from a fixed-arity list. + * Leading parameters may be optional; their types then already include `null` + * (see `Elaborator.optionalType`). A variadic parameter's type describes the whole + * array it receives, and its trailing parameters are required. */ -final class DeclaredParams(val params: Ls[Opt[DeclaredType]], val hasRest: Bool, val rest: Opt[DeclaredType]) extends ValueKey: - override def toString: Str = s"DeclaredParams($params, $hasRest, $rest)" - protected val valueHash: Int = ValueKey.hash(ValueKey.tag(this), params.hashCode, hasRest.hashCode, rest.hashCode) +final class DeclaredParams(val params: Ls[DeclaredParam], val variadic: Opt[Variadic[DeclaredParam]]) extends ValueKey: + def hasRest: Bool = variadic.isDefined + def rest: Opt[DeclaredType] = variadic.flatMap(_.param.tpe) + def trailing: Ls[DeclaredParam] = variadic.fold(Nil)(_.trailing) + /** The same leading, rest, and trailing order as `ParamList.allParams`. */ + lazy val allParams: Ls[DeclaredParam] = params ::: variadic.fold(Nil)(_.allParams) + /** The number of leading required (i.e., non-optional) parameters. */ + lazy val leadingRequired: Int = FldFlags.requiredPrefixLength(params.map(_.flags)) + override def toString: Str = s"DeclaredParams($params, $variadic)" + protected val valueHash: Int = ValueKey.hash(ValueKey.tag(this), params.hashCode, variadic.hashCode) protected def sameValue(other: ValueKey): Bool = other match - case that: DeclaredParams => params == that.params && hasRest == that.hasRest && rest == that.rest + case that: DeclaredParams => params == that.params && variadic == that.variadic case _ => false + 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, @@ -792,7 +843,7 @@ final case class CallableTypeShape(source: Term, paramLists: NELs[DeclaredParams 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) - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = MemberLookup.Missing + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = MemberLookup.Missing /** Why observing a type did not expose a member interface. Keep this evidence * separate from semantic candidates: normalization can synthesize Any without @@ -841,13 +892,13 @@ final class OpaqueTypeShape(val source: Term)(val reason: TypeInterfaceReason) e override def toString: Str = s"OpaqueTypeShape($source)" def toLoc: Opt[Loc] = source.toLoc def provenance: ShapeProvenance = reason.provenance - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = MemberLookup.Unknown(MemberLookup.Uncertainty.ValueShape, false, provenance) class DefnShape(val defn: Definition, val ext: Opt[TermShape]) extends CoreHeadShape: /** Instance lookup is shared by constructor calls and explicit `new`. * Inherited members retain their marks before the caller adds its captures. */ - def getInstanceMember(name: Str)(using ResolutionContext): MemberLookup = defn match + def getInstanceMember(name: MemberKey)(using ResolutionContext): MemberLookup = defn match case cd: ClassDef => MemberLookup.inClass(cd, ext, name) case _: TermDefinition => ext.fold[MemberLookup](MemberLookup.Missing)(_.getMember(name)) case _ => MemberLookup.Missing @@ -902,18 +953,20 @@ class DefnShape(val defn: Definition, val ext: Opt[TermShape]) extends CoreHeadS val ctor = (cls.paramsOpt.toList ::: cls.auxParams).foldRight[Term](Term.Lit(syntax.Tree.IntLit(0))): (params, body) => scoped(Term.Lam(params, body)) val methods = cls.body.methods.filter(method => method.body.nonEmpty && !method.tsym.isPrivate).map: method => - val ref = scoped(Term.MemberRef(method.bsym)(new syntax.Tree.Ident(method.bsym.nme), FlowSymbol.memSym(method.bsym))) + val ref = scoped(Term.MemberRef(method.bsym)(N, FlowSymbol.memSym(method.bsym))) field(method.bsym.nme, withBuffer(ref)) (field("size", Term.Lit(syntax.Tree.IntLit(0))) :: field("ctor", withBuffer(ctor)) :: methods).toMap case _ => Map.empty - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = defn match case defn: ModuleOrObjectDef => MemberLookup.inClass(defn, ext, name) case cls: ClassDef if cls.annotations.exists(_.isInstanceOf[Annot.Bufferable]) => - bufferedMembers.get(name).fold[MemberLookup](MemberLookup.Missing)(MemberLookup.Found(_, Nil)) + name match + case name: Str => bufferedMembers.get(name).fold[MemberLookup](MemberLookup.Missing)(MemberLookup.Found(_, Nil)) + case _: SymbolKey => MemberLookup.Missing case _: TermDefinition => MemberLookup.Missing case _ => MemberLookup.Missing /** A definition is identified by its header; a class constructor, by its class's. @@ -969,7 +1022,7 @@ final class TupleShape private (val source: Term, val elements: Ls[TupleShape.El def describe: Str = "tuple literal" override def toString: Str = s"TupleShape($source, $elements, $instances)" def toLoc: Opt[Loc] = source.toLoc - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = name.toIntOption match + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = MemberKey.index(name) match case S(index) if index >= 0 => // An unknown-length segment can be empty. After reaching one, every // following field whose minimum position is at most `index` is possible. @@ -987,7 +1040,7 @@ final class TupleShape private (val source: Term, val elements: Ls[TupleShape.El case Nil => MemberLookup.Missing case fields => MemberLookup.Indexed(fields, Nil) case _ => arrayParent.getMember(name) - override def getMemberThrough(name: Str, receiver: Marks)(using ResolutionContext): MemberLookup = name.toIntOption match + override def getMemberThrough(name: MemberKey, receiver: Marks)(using ResolutionContext): MemberLookup = MemberKey.index(name) match case S(index) if index >= 0 => super.getMemberThrough(name, receiver) case _ => arrayParent.getMemberThrough(name, receiver) @@ -1000,7 +1053,7 @@ final class DynShape private () extends CoreHeadShape: def describe: Str = "dynamic value" override def toString: Str = "DynShape()" def toLoc: Opt[Loc] = N - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = MemberLookup.Dynamic(Nil) + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = MemberLookup.Dynamic(Nil) object DynShape: private val instance = new DynShape @@ -1016,8 +1069,9 @@ final class ShapeProvenance private (path: => Ls[(Message, Opt[Loc])], notes: => private lazy val originNotes = notes /** A type origin anchors the operation's explanation: first trace the value * back to that annotation, then explain the interface it provides. Without - * an annotation witness, report the operation at its fallback location first - * and follow it with the reasons the value's shape is unknown. + * an annotation witness, lead with the operation's explanation and follow it + * with the reasons the value's shape is unknown. The fallback identifies other + * shape evidence, if any, not the operation already located by the main error. */ def diagnostic(message: Message, fallback: Opt[Loc]): Ls[(Message, Opt[Loc])] = typeOrigin match case S(source) => pathNotes ::: (message -> source.toLoc.orElse(fallback)) :: originNotes @@ -1047,7 +1101,7 @@ final class UnknownValueShape(val source: Term, val fromPublicInterface: Bool)(v def describe: Str = "value of unknown shape" override def toString: Str = s"UnknownValueShape($source, $fromPublicInterface)" def toLoc: Opt[Loc] = source.toLoc - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = MemberLookup.Unknown(MemberLookup.Uncertainty.ValueShape, fromPublicInterface, provenance) /** A generic definition must be valid without choosing a caller's type. This @@ -1060,7 +1114,7 @@ final class RigidTypeShape(val parameter: VarSymbol, val source: Term) extends C def toLoc: Opt[Loc] = source.toLoc def provenance: ShapeProvenance = ShapeProvenance( msg"Type parameter '${parameter.nme}' does not specify a member interface." -> parameter.toLoc :: Nil) - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = MemberLookup.Unknown(MemberLookup.Uncertainty.ValueShape, false, provenance) object UnknownValueShape: @@ -1159,8 +1213,8 @@ object TupleShape: protected def sameValue(other: ValueKey): Bool = other match case that: Rest => shape == that.shape && segments == that.segments case _ => false - /** A view of `tuple` retaining only `segments`, which must be taken from - * `tuple.segments`. A view of another view refers to that view's parent: + /** A view of `tuple` retaining the fields in `segments`, possibly with their + * lengths widened by rest slicing. A view of another view refers to that view's parent: * a view contributes no spreads of its own, so containsSpread gives the same * answer for the parent as for the intermediate view. Nesting views instead * would make the candidate's identity record the order in which fields were @@ -1189,20 +1243,23 @@ final class RecordShape private (val source: Term.Rcd, val elements: Ls[RecordSh def describe: Str = "record literal" override def toString: Str = s"RecordShape($source, $elements, $instances)" def toLoc: Opt[Loc] = source.toLoc - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = import MemberLookup.* def unknown(source: Located) = Unknown(Uncertainty.RecordOverwrite, false, ShapeProvenance(msg"This computed key can overwrite the selected member." -> source.toLoc :: Nil)) def loop(rest: Ls[RecordShape.Element]): MemberLookup = rest match case Nil => Missing - case RecordShape.Field(field) :: rest => field.field match - case Term.Lit(Tree.StrLit(key)) => + case RecordShape.Field(field) :: rest => field.key match + case S(key) => if key == name then Found(RecordMember(field, 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) case RecordShape.Spread(shape, marks) :: rest => def spread(info: MemberLookup): Opt[MemberLookup] = info match + case All(members) => members.flatMap(spread) match + case Nil => N + case members => S(MemberLookup.all(members)) case Contextual(source, instances) => spread(source).map(_.instantiate(instances)) case Dynamic(inner) => S(Dynamic(inner ::: marks :: Nil)) case Unknown(_, fromPublicInterface, provenance) => S(Unknown(Uncertainty.RecordOverwrite, fromPublicInterface, provenance)) @@ -1263,9 +1320,9 @@ class IntroShape(val trm: IntroTerm, val primitive: Opt[NominalInstanceView]) ex def describe: Str = trm.describe // Literals have global primitive interfaces; lambdas capture their scopes. override def isSelfContained: Bool = !trm.isInstanceOf[Term.Lam] - protected def getMemberImpl(name: Str)(using ResolutionContext): MemberLookup = + protected def getMemberImpl(name: MemberKey)(using ResolutionContext): MemberLookup = primitive.fold[MemberLookup](MemberLookup.Missing)(_.getMember(name)) - override def getMemberThrough(name: Str, receiver: Marks)(using ResolutionContext): MemberLookup = + override def getMemberThrough(name: MemberKey, receiver: Marks)(using ResolutionContext): MemberLookup = primitive.fold[MemberLookup](MemberLookup.Missing)(_.getMemberThrough(name, receiver)) override def isInstanceOfClass(cls: ClassLikeDef)(using ResolutionContext): Bool = primitive.exists(_.isInstanceOfClass(cls)) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala index 859999e611..17b79eec4c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/SimpleSplit.scala @@ -123,7 +123,7 @@ object SimpleSplit: enum Head extends AutoLocated: // Elaboration binds scrutinee expressions first: this reference must name // a LocalVarSymbol, allowing its sub-scrutinees to be shared by binding. - case Match(scrutinee: Term.Ref, pattern: Pattern, consequent: SimpleSplit) + case Match(scrutinee: Term.Ref | Term.SimpleRef, pattern: Pattern, consequent: SimpleSplit) case Let(binding: LocalVarSymbol, term: Term) def show(using Scope, ShowCfg, Raise, IfLikeForm): Document = this match diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala index b6d7cbfec7..57bd49a189 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Split.scala @@ -2,20 +2,18 @@ package hkmc2 package semantics import hkmc2.utils.*, shorthands.* -import syntax.*, Elaborator.State, ucs.FlatPattern +import syntax.*, Elaborator.State, ucs.FlatPattern, ups.SplitCompiler.SymbolScrut -final case class Branch(scrutinee: Term.Ref, pattern: FlatPattern, continuation: Split) extends AutoLocated: +final case class Branch(scrutinee: Term.SimpleRef, pattern: FlatPattern, continuation: Split) extends AutoLocated: def mkClone(using State, codegen.Erasure): Branch = - val scrutineeClone = new Term.Ref(scrutinee.sym) - (Tree.Ident(scrutinee.tree.name), scrutinee.typ) - Branch(scrutineeClone, pattern.mkClone, continuation.mkClone) + Branch(scrutinee.mkClone.asInstanceOf, pattern.mkClone, continuation.mkClone) override def children: Vector[Located] = Vector.triple(scrutinee, pattern, continuation) def showDbg(using DebugPrinter): String = s"${scrutinee.sym.nme} is ${pattern.showDbg} -> { ${continuation.showDbg} }" object Branch: - def apply(scrutinee: Term.Ref, continuation: Split): Branch = + def apply(scrutinee: Term.SimpleRef, continuation: Split): Branch = Branch(scrutinee, FlatPattern.Lit(Tree.BoolLit(true)), continuation) enum Split extends AutoLocated with ProductWithTail: @@ -201,7 +199,7 @@ object Split: bindings.iterator.foldLeft(go(consequent)): case (innerSplit, (symbol, mkTerm)) => Let(symbol, mkTerm(), innerSplit) compiler.makeMatchSplit - (scrutCache.getOrElseUpdate(symbol, Scrut.from(ref)), pattern, false) + (scrutCache.getOrElseUpdate(symbol, SymbolScrut(symbol)), pattern, false) (makeConsequent, alternative) case SS.Head.Let(binding, term) => Let(binding, term, go(tail)) case SS.Else(default) => Else(default) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala index d4f1bfe2ec..6e4073aa4b 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Symbol.scala @@ -293,7 +293,6 @@ final class TypeParameterInstance(val origin: VarSymbol, val siteBinders: Opt[Se extends VarSymbol(origin.id): require(!origin.isInstanceOf[TypeParameterInstance], "Instantiate original type binders only") decl = origin.decl - val reference: Term.SimpleRef = Term.SimpleRef(this)(origin.id) class BuiltinSymbol (val nme: Str, val binary: Bool, val unary: Bool, val nullary: Bool, val functionLike: Bool, val isPure: Bool)(using State) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala index 053024dc9e..8b9ce2bcf0 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/Term.scala @@ -25,6 +25,8 @@ final case class QuantVar(sym: VarSymbol, ub: Opt[Term], lb: Opt[Term]) enum Annot extends Located: val toLoc: Opt[Loc] + case JsSymbol(key: SymbolKey)(val toLoc: Opt[Loc]) + case Fresh()(val toLoc: Opt[Loc]) case Untyped()(val toLoc: Opt[Loc]) case Modifier(mod: Keyword)(val toLoc: Opt[Loc]) case Trm(trm: Term)(val toLoc: Opt[Loc]) @@ -61,10 +63,13 @@ enum Annot extends Located: def subTerms: Vector[Term] = this match case Trm(trm) => Vector.single(trm) - case _: Modifier | Untyped() | TailRec() | TailCall() | Inline() | NoInline() + case JsSymbol(key) => Vector.single(key.term) + case Fresh() | _: Modifier | Untyped() | TailRec() | TailCall() | Inline() | NoInline() | Generator() | Async() | RaiseEffects() | Pure() | _: Bufferable | _: Config | _: Debug | _: Affine => Vector.empty def show(using Scope, ShowCfg, Raise): Document = this match + case JsSymbol(key) => doc"@jsSymbol(${key.term.show})" + case Fresh() => doc"@annotations.fresh" case Untyped() => doc"@untyped" case Inline() => doc"@inline" case NoInline() => doc"@noInline" @@ -83,6 +88,7 @@ enum Annot extends Located: def mkClone(using State, Erasure): Annot = this match case Trm(trm) => Trm(trm.mkClone)(toLoc) + case JsSymbol(key) => JsSymbol(key.copy()(key.term.mkClone))(toLoc) case _ => this object Annot: @@ -277,36 +283,6 @@ object Resolvable: case class SrcScope(outer: Elaborator.OuterCtx, parent: Opt[SrcScope]): - /** Computes the outermost scope from which the current scope can still be accessed. - * For instance, from [scp2] here, [scp1] is the outermost accessible base: - * [scp0] - * fun foo = - * [scp1] - * module Foo with - * module Bar with - * [scp2] - * and the path is Foo :: Bar :: Nil. - * [scp0] cannot access [scp2] because there is a function (Function outer) on the way. - * The same would happen for: - * [scp0] - * if ... then // LocalScope also blocks access to [scp1] and [scp2] - * [scp1] - * module Foo with - * module Bar with - * [scp2] - */ - lazy val outermostAcessibleBase: (SrcScope, Ls[InnerSymbol]) = - import Elaborator.OuterCtx.* - outer match - case InnerScope(inner) => - parent match - case N => (this, inner :: Nil) - case S(par) => - val (base, path) = par.outermostAcessibleBase - (base, inner :: path) - case _: (Function | LocalScope | NonReturnContext) | LambdaOrHandlerBlock => - (this, Nil) - /** Whether code written in this scope can access the private members of `owner`: whether it is nested in the * definition of `owner` or of one of its companions, which are the other class-like definitions of its block member * symbol. These are found through `owner`, whose definition is complete, unlike the definitions enclosing this scope, @@ -332,9 +308,9 @@ enum Term extends Statement, AutoLocated, ShapePublisher: case Lit(lit: Literal) // --- NEW --- - case SimpleRef(sym: SimpleRefSymbol)(val tree: Tree.Ident) extends Term, NewRefImpl + case SimpleRef(sym: SimpleRefSymbol)(val tree: Opt[Tree.Ident]) extends Term, NewRefImpl case SelfRef(sym: InnerSymbol)(val tree: Tree.Ident) extends Term, NewRefImpl - case MemberRef(sym: MemberSymbol)(val tree: Tree.Ident, val resSym: FlowSymbol) extends Term, NewResolvableImpl, NewRefImpl + case MemberRef(sym: MemberSymbol)(val tree: Opt[Tree.Ident], val resSym: FlowSymbol) extends Term, NewResolvableImpl, NewRefImpl /** A direct reference to an inherited implementation, retaining the lexical receiver. */ case Super(receiver: Term, owner: InnerSymbol, id: Tree.Ident)(val resSym: FlowSymbol) extends Term, NewResolvableImpl, ShapeHost /** An optional class selection fixes the lookup scope for an explicit member projection. */ @@ -360,7 +336,7 @@ enum Term extends Statement, AutoLocated, ShapePublisher: // --- END --- case App(lhs: Term, rhs: Term) - (val tree: Tree.App, val typ: Opt[Type], val resSym: FlowSymbol) extends Term, ResolvableImpl + (val tree: Tree.App, val typ: Opt[Type], val resSym: FlowSymbol) extends Term, ResolvableImpl, AppImpl case TyApp(lhs: Term, targs: Ls[Term]) (val typ: Opt[Type]) extends Term, ResolvableImpl /** `checked` asks resolution to validate the receiver and determine the element @@ -411,7 +387,9 @@ enum Term extends Statement, AutoLocated, ShapePublisher: case Try(body: Term, finallyDo: Term) case Annotated(annot: Annot, target: Term) case Handle(lhs: LocalVarSymbol, rhs: Term, args: List[Term], - derivedClsSym: ClassSymbol, defs: Ls[HandlerTermDefinition], body: Term) + derivedClsSym: ClassSymbol, defs: Ls[HandlerTermDefinition], body: Term)( + // Abortive handlers never resume their body: a method's result is the handled expression's result. + val isAbortive: Bool) case LeadingDotSel(nme: Tree.Ident)( val originalCtx: Opt[SrcScope] ) (using State) extends Term, ResolvableImpl, LeadingDotSelImpl @@ -587,6 +565,7 @@ enum Term extends Statement, AutoLocated, ShapePublisher: copyResolution(source, copy) def copyNewResolution(source: NewResolvable, copy: NewResolvable): copy.type = copy.resolvedTargets = source.resolvedTargets + copy.keyAccess = source.keyAccess.map(_.mkClone) copy.isErroneous = source.isErroneous copy // Keep class qualifiers typed as selections while sharing all clone metadata copying. @@ -626,7 +605,10 @@ enum Term extends Statement, AutoLocated, ShapePublisher: case Capture(base, thru) => Capture(base.mkClone, thru) case term @ Resolved(t, sym) => copyResolution(term, Resolved(t.mkClone, sym)(term.typ)) case term @ Ref(sym) => copyResolution(term, Ref(sym)(Tree.Ident(term.tree.name), term.typ)) - case term @ App(lhs, rhs) => copyResolution(term, App(lhs.mkClone, rhs.mkClone)(term.tree, term.typ, term.resSym)) + case term @ App(lhs, rhs) => + val copy = App(lhs.mkClone, rhs.mkClone)(term.tree, term.typ, term.resSym) + copy.callableTargets.shapes.copyFrom(term.callableTargets.shapes) + copyResolution(term, copy) case term @ TyApp(lhs, targs) => copyResolution(term, TyApp(lhs.mkClone, targs.map(_.mkClone))(term.typ)) case term @ Sel(prefix, nme) => copySelection(term, Sel(prefix.mkClone, Tree.Ident(nme.name))(term.sym, term.resSym, term.typ, term.originalCtx)) case term @ SynthSel(prefix, nme) => copySelection(term, SynthSel(prefix.mkClone, Tree.Ident(nme.name))(term.sym, term.resSym, term.typ, term.originalCtx)) @@ -683,8 +665,8 @@ enum Term extends Statement, AutoLocated, ShapePublisher: case Continue(label) => Continue(label) case Try(body, finallyDo) => Try(body.mkClone, finallyDo.mkClone) case Annotated(annot, target) => Annotated(annot, target.mkClone) - case Handle(lhs, rhs, args, derivedClsSym, defs, body) => - Handle(lhs, rhs.mkClone, args.map(_.mkClone), derivedClsSym, defs, body.mkClone) + case handle @ Handle(lhs, rhs, args, derivedClsSym, defs, body) => + Handle(lhs, rhs.mkClone, args.map(_.mkClone), derivedClsSym, defs, body.mkClone)(handle.isAbortive) copyMetadata(this, that) end Term @@ -769,7 +751,7 @@ final case class LetDecl(sym: LocalVarSymbol | TermSymbol, annotations: Ls[Annot * to evaluate its value. Computed keys also have an identity, but cannot be * selected statically until their key is known. Cloning preserves this identity. */ -final case class RcdField(field: Term, rhs: Term, sym: BlockMemberSymbol) extends Statement, AutoLocated: +final case class RcdField(field: Term, rhs: Term, sym: BlockMemberSymbol)(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 @@ -779,9 +761,11 @@ final case class RcdField(field: Term, rhs: Term, sym: BlockMemberSymbol) extend case _ => () object RcdField: - def apply(field: Term, rhs: Term)(using State): RcdField = make(field, rhs, false) - def signature(field: Term, sign: Term)(using State): RcdField = make(field, sign, true) - private def make(field: Term, rhs: Term, signature: Bool)(using State): 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 = val name = field match case Term.Lit(Tree.StrLit(name)) => name case _ => "computed field" @@ -795,9 +779,11 @@ object RcdField: // directly to its written type: projection does not invoke a value definition. tsym.defn = S(TermDefinition(RecordField, sym, tsym, Nil, N, if signature then S(rhs) else N, - if signature then N else S(Term.Capture(rhs, tsym)), TermDefFlags.empty, Modulefulness.none, Nil, N)(field.toLoc, field.toLoc)) + 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) + RcdField(field, rhs, sym)(symbol.orElse(field match + case Term.Lit(Tree.StrLit(name)) => S(name) + case _ => N)) final case class RcdSpread(rcd: Term) extends Statement, AutoLocated final case class DefineVar(sym: LocalSymbol | TermSymbol, rhs: Term)(val toLoc: Opt[Loc]) extends Statement @@ -876,7 +862,10 @@ final case class TermDefinition( modulefulness: Modulefulness, annotations: Ls[Annot], companion: Opt[CompanionSymbol], -)(val toLoc: Opt[Loc], val headerLoc: Opt[Loc]) extends CompanionValue: +)(val toLoc: Opt[Loc], val headerLoc: Opt[Loc], + // Lexical binding site, as ModuleOrObjectDef.path; absent for synthetic definitions. + val scope: Opt[SrcScope], +) extends CompanionValue: require(k is tsym.k) def bsym: BlockMemberSymbol = sym val owner = tsym.owner @@ -1216,18 +1205,34 @@ case class TypeDef( // TODO Store optional source locations for the flags instead of booleans -final case class FldFlags(mut: Bool, spec: Bool, pat: Bool, isVal: Bool): +/** `opt` marks an optional parameter (`y?`) or an optional function type parameter (`Int?`), + * whose argument may be omitted by callers, in which case the parameter is `null`. + * Unlike the other flags, which are prefix modifiers, it is written as a postfix `?`, + * so it is not part of `show` and is printed by `showOptMarker` after the name instead. + * `droppable` marks a parameter (`drop y`) or function type parameter (`drop Int`) whose argument + * is always passed but which implementations may ignore, such as the index passed by `Array#map` + * to its callback. Resolution permits callbacks to omit these parameters; + * code generation already ignores extra arguments (see `Runtime.checkArgs`). */ +final case class FldFlags(mut: Bool, spec: Bool, pat: Bool, isVal: Bool, opt: Bool, droppable: Bool): def show(addSpace: Bool = false): Str = val flags = Buffer.empty[String] if mut then flags += "mut" if spec then flags += "spec" if pat then flags += "pattern" if isVal then flags += "val" + if droppable then flags += "drop" flags.mkString(" ") + (if addSpace && flags.nonEmpty then " " else "") - override def toString: String = "‹" + show(false) + "›" + def showOptMarker: Str = if opt then "?" else "" + override def toString: String = "‹" + show(false) + showOptMarker + "›" object FldFlags: - val empty: FldFlags = FldFlags(false, false, false, false) + val empty: FldFlags = FldFlags(false, false, false, false, false, false) + /** The number of required (i.e., non-optional) parameters at the start of a parameter list. + * Elaboration reports required parameters following optional ones; these are counted as optional, + * since arguments are assigned to parameters positionally. */ + def requiredPrefixLength(flags: Ls[FldFlags]): Int = flags.indexWhere(_.opt) match + case -1 => flags.length + case n => n object benign: // * Some flags like `mut` and `module` are "benign" in the sense that they don't affect code-gen def unapply(flags: FldFlags): Bool = @@ -1249,7 +1254,7 @@ enum IfLikeForm: sealed abstract class Elem extends AutoLocated: def subTerms: Ls[Term] = this match case Fld(_, term, asc) => term :: asc.toList - case Spd(_, term) => term :: Nil + case Spd(_, term, _) => term :: Nil def show(using Scope, ShowCfg, Raise): Document def showDbg(using DebugPrinter): Str object Elem: @@ -1258,24 +1263,18 @@ final case class Fld(flags: FldFlags, term: Term, asc: Opt[Term]) extends Elem, object PlainFld: def apply(term: Term) = Fld(FldFlags.empty, term, N) def unapply(fld: Fld): Opt[Term] = S(fld.term) -final case class Spd(k: SpreadKind, term: Term) extends Elem: - def show(using Scope, ShowCfg, Raise): Document = k.str :: term.show - def showDbg(using DebugPrinter): Str = k.str + term.showDbg +/** `droppable` marks the rest parameter of a function type, as in `(Int, drop ...Array[Int]) -> Int`, + * whose arguments implementations may ignore (see `FldFlags.droppable`). */ +final case class Spd(k: SpreadKind, term: Term, droppable: Bool) extends Elem: + private def dropStr = if droppable then "drop " else "" + def show(using Scope, ShowCfg, Raise): Document = dropStr :: k.str :: term.show + def showDbg(using DebugPrinter): Str = dropStr + k.str + term.showDbg def children: Vector[Located] = Vector.single(term) final case class TyParam(flags: FldFlags, vce: Opt[Bool], sym: VarSymbol)(val toLoc: Opt[Loc]) extends Declaration: - // * For variance analysis - var isCovariant: Bool = true - var isContravariant: Bool = true - def showDbg(using DebugPrinter): Str = - // (if isContravariant then "in " else "") + - // (if isCovariant then "out " else "") + - (if isCovariant then - if isContravariant then "" else "out " - else if isContravariant then "in " else "in out ") + - "‹" + flags.show + "›" + sym.showDbg + vce.fold("")(if _ then "out " else "in ") + "‹" + flags.show + "›" + sym.showDbg object Param: @@ -1299,28 +1298,50 @@ final case class Param(flags: FldFlags, sym: VarSymbol, sign: Opt[Term], modulef def show(using Scope, ShowCfg, Raise): Document = - doc"${flags.show(true)}${sym.showName}${sign.fold(doc"")(": " :: _.show)}" + doc"${flags.show(true)}${sym.showName}${flags.showOptMarker}${sign.fold(doc"")(": " :: _.show)}" - def showDbg(using DebugPrinter): Str = flags.show(true) + sym.showDbg + sign.fold("")(": " + _.showDbg) - -final case class ParamList(flags: ParamListFlags, params: Ls[Param], restParam: Opt[Param]) + def showDbg(using DebugPrinter): Str = + flags.show(true) + sym.showDbg + flags.showOptMarker + sign.fold("")(": " + _.showDbg) + +/** A variadic parameter, as in `fun f(x, ...xs, y, z)`, which receives the arguments passed between + * the leading parameters (`x`) and the `trailing` ones (`y` and `z`). The trailing parameters are + * always required. When there are no trailing parameters, the variadic parameter is a rest parameter. + * Lowering desugars trailing parameters into a rest parameter (see `Lowering.setupFunctionDef`). */ +final case class Variadic[+P](param: P, trailing: Ls[P]): + def map[Q](f: P => Q): Variadic[Q] = Variadic(f(param), trailing.map(f)) + def allParams: Ls[P] = param :: trailing + +/** `params` are the leading parameters, which may end with optional parameters. */ +final case class ParamList(flags: ParamListFlags, params: Ls[Param], variadic: Opt[Variadic[Param]]) (val toLoc: Opt[Loc]) extends Located: - def foreach(f: Param => Unit): Unit = (params.iterator ++ restParam).foreach(f) - def paramCountLB: Int = params.length - def paramCountUB: Bool = restParam.isEmpty - lazy val paramSyms = params.map(_.sym) ++ restParam.map(_.sym) - lazy val allParams = params ++ restParam.toList - def subTerms: Ls[Term] = params.flatMap(_.subTerms) ++ restParam.toList.flatMap(_.subTerms) + def foreach(f: Param => Unit): Unit = allParams.foreach(f) + def trailingParams: Ls[Param] = variadic.fold(Nil)(_.trailing) + /** The number of required leading parameters, which precede all optional parameters. */ + lazy val leadingRequiredCount: Int = FldFlags.requiredPrefixLength(params.map(_.flags)) + /** The minimum number of arguments: the required leading and trailing parameters. */ + def paramCountLB: Int = leadingRequiredCount + trailingParams.length + /** The maximum number of arguments, when there is no variadic parameter. */ + def paramCountMax: Int = params.length + trailingParams.length + def hasOptParams: Bool = leadingRequiredCount < params.length + def paramCountUB: Bool = variadic.isEmpty + lazy val allParams: Ls[Param] = params ::: variadic.fold(Nil)(_.allParams) + lazy val paramSyms = allParams.map(_.sym) + def subTerms: Ls[Term] = allParams.flatMap(_.subTerms) def show(using Scope, ShowCfg, Raise): Document = flags.show :: doc"(" :: ( params.map(_.show) ::: - restParam.map(p => doc"...${p.show}").toList + variadic.toList.flatMap(v => doc"...${v.param.show}" :: v.trailing.map(_.show)) ).mkDocument(", ") :: doc")" def showDbg(using DebugPrinter): Str = flags.showDbg - + (params.map(_.showDbg) ++ restParam.toList.map("..." + _.showDbg)).mkString("(", ", ", ")") + + (params.map(_.showDbg) ++ variadic.toList.flatMap(v => ("..." + v.param.showDbg) :: v.trailing.map(_.showDbg))) + .mkString("(", ", ", ")") +object ParamList: + /** A source parameter list with no trailing parameters after its variadic parameter. */ + def withRest(flags: ParamListFlags, params: Ls[Param], restParam: Opt[Param])(toLoc: Opt[Loc]): ParamList = + ParamList(flags, params, restParam.map(Variadic(_, Nil)))(toLoc) object PlainParamList: def apply(params: Ls[Param])(toLoc: Opt[Loc]) = ParamList(ParamListFlags.empty, params, N)(toLoc) diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala index bdcce84824..3e572ddd5a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/TermImpl.scala @@ -22,12 +22,29 @@ type AnyRef_ = AnyRefImpl & Term trait AnyRefImpl: self: Term.Ref | Term.SimpleRef | Term.MemberRef | Term.SelfRef => - def tree: Tree.Ident def sym: Symbol + /** An identifier records a written use, not a backend name. Desugared uses such + * as `_` can instead carry that token's location; wholly synthetic references + * have neither an identifier nor a source location. */ + def sourceId: Opt[Tree.Ident] = this match + case ref: Term.Ref => S(ref.tree) + case ref: Term.SelfRef => S(ref.tree) + case ref: Term.SimpleRef => ref.tree + case ref: Term.MemberRef => ref.tree trait NewRefImpl extends AnyRefImpl: self: Term.SimpleRef | Term.MemberRef | Term.SelfRef => +trait AppImpl: + self: Term.App => + /** Callable candidates observed by applyShape, independently of the call's results: + * a throwing function is a target even when it produces no value. These are diagnostic + * witnesses, not fixed code-generation decisions. A Publisher retains them across blocks + * and lets later activations add targets in the consuming unit's private inference data. + * completeBlock seals this host along with the application; mkClone copies its snapshot. + */ + private[semantics] val callableTargets: Publisher[TermShape] = new Publisher[TermShape] {} + trait NewSelImpl extends NewResolvableImpl: self: Term.NewSel => // At least one receiver permits runtime lookup without a static member symbol. @@ -60,8 +77,8 @@ trait UnresolvedRefImpl extends NewResolvableImpl: trait FldImpl: self: Fld => def children: Vector[Located] = self.term +: self.asc.toVector - def show(using Scope, ShowCfg, Raise): Document = flags.show(true) :: self.term.show - def showDbg(using DebugPrinter): Str = flags.show(true) + self.term.showDbg + def show(using Scope, ShowCfg, Raise): Document = flags.show(true) :: self.term.show :: flags.showOptMarker + def showDbg(using DebugPrinter): Str = flags.show(true) + self.term.showDbg + flags.showOptMarker def describe: Str = (if self.flags.spec then "specialized " else "") + (if self.flags.mut then "mutable " else "") + @@ -93,6 +110,9 @@ trait PossiblyErroneous: trait NewResolvableImpl extends PossiblyErroneous: self: MemberRef | NewSel | UnresolvedRef | Super => var resolvedTargets: Ls[DefinitionSymbol[?]] = Nil // * filled during flow analysis + // The implicit property-key expression, resolved in this use site's scope at + // block completion. Lowering must not reuse the annotation's lexical environment. + var keyAccess: Opt[Term] = N trait StatementImpl extends Located, ProductWithExtraInfo, Describable: @@ -103,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 RcdField(field, rhs, sym) => RcdField(field.mkClone, rhs.mkClone, sym) + case f @ RcdField(field, rhs, sym) => RcdField(field.mkClone, rhs.mkClone, sym)(f.key) case RcdSpread(rcd) => RcdSpread(rcd.mkClone) case DefineVar(sym, rhs) => DefineVar(sym, rhs.mkClone)(toLoc) case sc: SetConfig => sc @@ -190,6 +210,7 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: case Blk(stats, res) => stats.toVector :+ res case _ => subTerms def subTerms: Vector[Term] = this match + case ref: MemberRef => ref.keyAccess.toVector case Error() | Missing | _: Lit | _: AnyRef_ | _: DynTy | _: UnitVal => Vector.empty case Capture(base, thru) => Vector.single(base) case Resolved(t, sym) => Vector.single(t) @@ -200,9 +221,9 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: case TyApp(pre, tarsg) => pre +: tarsg.toVector case Sel(pre, _) => Vector.single(pre) case SynthSel(pre, _) => Vector.single(pre) - case Super(receiver, _, _) => Vector.single(receiver) - case NewSel(pre, _, cls) => Vector.single(pre) ++ cls.toVector - case UnresolvedRef(prefixes, _) => prefixes.toVector + case sup @ Super(receiver, _, _) => Vector.single(receiver) ++ sup.keyAccess.toVector + case sel @ NewSel(pre, _, cls) => Vector.single(pre) ++ cls.toVector ++ sel.keyAccess.toVector + case ref @ UnresolvedRef(prefixes, _) => prefixes.toVector ++ ref.keyAccess.toVector case DynSel(o, f, _, _) => Vector.double(o, f) case Tup(fields) => fields.flatMap(_.subTerms).toVector case Mut(und) => Vector.single(und) @@ -261,7 +282,7 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: protected def children: Vector[Located] = this match case t: Lit => Vector.single(t.lit.asTree) - case t: AnyRef_ => treeOrSubterms(t.tree) + case t: AnyRef_ => t.sourceId.toVector case t: Tup => treeOrSubterms(t.tree) case l: Lam => Vector.double(l.params, l.body) case t: App => treeOrSubterms(t.tree) @@ -464,6 +485,11 @@ trait StatementImpl extends Located, ProductWithExtraInfo, Describable: case r: Ref => r.sym.showAsPlain case r: Resolved => s"${r.showPlain}‹${r.sym.showDbg}›" + case sel: NewSel => + // New selections keep their targets outside the legacy `symbol` field. + // Debug output can run during resolution, so show all currently known targets. + val targets = sel.resolvedTargets.distinct + sel.showPlain + (if targets.isEmpty then "" else targets.map(_.showDbg).mkString("‹", ", ", "›")) case trm: Term => // s"$showPlain‹${trm.symbol.getOrElse("")}›" s"$showPlain${trm.symbol.fold("")(sym => "‹"+sym.showDbg+"›")}" diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/TypeShape.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/TypeShape.scala index 636a8b3252..0387e4a471 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/TypeShape.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/TypeShape.scala @@ -45,7 +45,8 @@ object TypeShape: protected def sameValue(other: ValueKey): Bool = other match case that: Combined => formula == that.formula case _ => false - final case class Tuple(fields: Ls[TypeResolution]) extends TypeShape: + final case class TupleField(tpe: TypeResolution, spread: Bool) + final case class Tuple(fields: Ls[TupleField]) extends TypeShape: protected val valueHash: Int = ValueKey.hash(ValueKey.tag(this), fields.hashCode) protected def sameValue(other: ValueKey): Bool = other match case that: Tuple => fields == that.fields @@ -184,10 +185,13 @@ final class TypeResolution(val source: Term, report: Ls[(Message, Opt[Loc])] => def fail(messages: Ls[(Message, Opt[Loc])]): Unit = if !reported then reported = true report(messages) - def validate(seen: Set[TypeResolution])(using Erasure): Unit = if !seen(this) then + /** Resolution is complete, so each node needs one check per erasure pass. + * A path-local cycle guard would revisit shared alias bodies through every + * field that references them, expanding a small graph into an exponential tree. + */ + def validate(visited: scala.collection.mutable.Set[TypeResolution])(using Erasure): Unit = if visited.add(this) then import TypeShape.* import Message.MessageContext - val next = seen + this val ambiguousReceiver = source.withoutCaptures match case ref: Term.UnresolvedRef => ref.resolvedMembers.distinct.sizeCompare(1) > 0 case sel: Term.NewSel => sel.hasAmbiguousClass @@ -206,11 +210,11 @@ final class TypeResolution(val source: Term, report: Ls[(Message, Opt[Loc])] => case _ => declarations(this, Set.empty).distinct fail(msg"This type is ambiguous, as it has multiple resolved targets" -> source.toLoc :: candidates) shapes.foreachGrowing: - case Alias(_, rhs) => rhs.foreach(_.validate(next)) - case Reference(base, _, _, _) => base.validate(next) + case Alias(_, rhs) => rhs.foreach(_.validate(visited)) + case Reference(base, _, _, _) => base.validate(visited) case Applied(base, args) => - base.validate(next) - args.foreach(_.validate(next)) + base.validate(visited) + args.foreach(_.validate(visited)) def checkArity(base: TypeResolution, seen: Set[TypeResolution]): Unit = if !seen(base) then def check(name: Str, count: Int, loc: Opt[Loc]): Unit = if args.length > count then fail(msg"Type '$name' accepts at most $count type ${"argument".pluralized(count)}, but got ${args.length}." -> @@ -221,22 +225,22 @@ final class TypeResolution(val source: Term, report: Ls[(Message, Opt[Loc])] => case Reference(inner, _, _, _) :: Nil => checkArity(inner, seen + base) case _ => () checkArity(base, Set.empty) - case Wildcard(input, output) => input.foreach(_.validate(next)); output.foreach(_.validate(next)) - case Argument(parts) => parts.input.resolution.validate(next); parts.output.resolution.validate(next) - case SelectedArgument(argument, _) => argument.resolution.validate(next) - case Contextual(reference) => reference.tpe.resolution.validate(next) - case Function(_, result) => result.validate(next) + case Wildcard(input, output) => input.foreach(_.validate(visited)); output.foreach(_.validate(visited)) + case Argument(parts) => parts.input.resolution.validate(visited); parts.output.resolution.validate(visited) + case SelectedArgument(argument, _) => argument.resolution.validate(visited) + case Contextual(reference) => reference.tpe.resolution.validate(visited) + case Function(_, result) => result.validate(visited) case Polymorphic(params, _, body) => params.foreach: param => - param.lower.foreach(_.validate(next)) - param.upper.foreach(_.validate(next)) - body.validate(next) - case Tuple(fields) => fields.foreach(_.validate(next)) - case Record(_, fields) => fields.foreach(_._2.validate(next)) - case Union(left, right) => left.validate(next); right.validate(next) - case Intersection(left, right) => left.validate(next); right.validate(next) - case Negation(base) => base.validate(next) - case Combined(formula) => formula.orderedAtoms.foreach(_.resolution.validate(next)) + param.lower.foreach(_.validate(visited)) + param.upper.foreach(_.validate(visited)) + body.validate(visited) + case Tuple(fields) => fields.foreach(_.tpe.validate(visited)) + case Record(_, fields) => fields.foreach(_._2.validate(visited)) + case Union(left, right) => left.validate(visited); right.validate(visited) + case Intersection(left, right) => left.validate(visited); right.validate(visited) + case Negation(base) => base.validate(visited) + case Combined(formula) => formula.orderedAtoms.foreach(_.resolution.validate(visited)) case _ => () /** A type with its lexical type-parameter bindings. These bindings describe declared diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/flow/FlowAnalysis.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/flow/FlowAnalysis.scala index 7133a02075..7fa6fd3398 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/flow/FlowAnalysis.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/flow/FlowAnalysis.scala @@ -129,7 +129,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): val prod = cd.paramsOpt match case S(ps) => - ps.restParam match + ps.variadic match case S(_) => ??? case N => P.Fun( @@ -248,7 +248,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): C.Flow(p.flow) def typeParamList(ps: ParamList): Ls[C] = - if ps.restParam.nonEmpty then + if ps.variadic.nonEmpty then ??? // TODO ps.params.map(typeParam) @@ -264,7 +264,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): Type.Tup: fields.map: case f: Fld => typeType(f.term) - case Spd(eager, term) => (???, typeType(term)) + case Spd(eager, term, _) => (???, typeType(term)) case FunTy(lhs, rhs, _) => Type.Fun(typeType(lhs), typeType(rhs), N) case _ => TODO(t) @@ -325,7 +325,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): d.body.members.get(name) match case S(memb: BlockMemberSymbol) => oc - .flatMap(findAccessPath(_, d.path, ms)) + .flatMap(scope => AccessPath.findAccessPath(scope, ms).toOption) .map((_, memb)) case _ => N case cs: ClassSymbol => @@ -336,7 +336,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): d.body.members.get(name) match case S(memb: BlockMemberSymbol) => oc - .flatMap(findAccessPath(_, d.path, comp)) + .flatMap(scope => AccessPath.findAccessPath(scope, comp).toOption) .map((_, memb)) case _ => N case _ => N @@ -461,7 +461,7 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): log(dbg"Found companion member ${memb}") sel.trm.originalCtx match case S(oc) => - val patho = findAccessPath(oc, cd.path, comp) + val patho = AccessPath.findAccessPath(oc, comp).toOption log(dbg"Access path: ${patho}") patho match case S(path) => @@ -497,26 +497,6 @@ class FlowAnalysis(using tl: TraceLogger)(using Raise, State, Ctx): raise(ErrorReport( msg"Could not solve all constraints within $MAX_FUEL iterations." -> N :: Nil)) - def findAccessPath(src: SrcScope, dst: SrcScope, moduleSym: ModuleOrObjectSymbol): Opt[Term] = - log(dbg"outermostAcessibleBase ${dst.outermostAcessibleBase}") - val (outermostBase, outermostPath) = dst.outermostAcessibleBase - var cur = src - while cur isnt outermostBase do - cur.parent match - case N => - return N - case S(p) => - cur = p - assert(cur is outermostBase) - (moduleSym :: outermostPath).reverse match - case Nil => die - case sym :: syms => S: - syms.foldLeft(sym.asInstanceOf[DefinitionSymbol[?]]//FIXME - .bms.getOrElse(die).ref(): Term): (a, b) => - Sel(a, Tree.Ident(b.nme))(S(b.asInstanceOf[DefinitionSymbol[?]]//FIXME - .bms.getOrElse(die)), FlowSymbol.sel(b.nme), N, N) - - import hkmc2.document.* import utils.Scope diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/FlatPattern.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/FlatPattern.scala index 70396e32ba..ae67ba29b6 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/FlatPattern.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/FlatPattern.scala @@ -74,9 +74,8 @@ enum FlatPattern extends AutoLocated: case ClassLike(ctor, symbol, args, rfd) => def showCtor(ctor: Term): Str = ctor match // This prints the symbol name without `refNum` and "member:" prefix. - case Term.Ref(sym: BlockMemberSymbol) => sym.nme - // This prints the symbol without `refNum`. - case Term.Ref(sym) => sym.toString + case ref: AnyRef_ => ref.sym.nme + case Term.NewSel(p, i, N) => s"${showCtor(p)}.${i.name}" case Term.Sel(p, i) => s"${showCtor(p)}.${i.name}" case Term.SynthSel(p, i) => s"${showCtor(p)}.${i.name}" case _ => ctor.showDbg 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 2d472115b5..c6c76945e7 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/Normalization.scala @@ -99,7 +99,7 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C extension (split: Split) /** Check if any branch in the split tests the given scrutinee. */ - private def referencesScrutinee(scrutinee: Term.Ref): Bool = split match + private def referencesScrutinee(scrutinee: Term.SimpleRef): Bool = split match case Split.Cons(Branch(thatScrutinee, _, continuation), tail) => (scrutinee.sym is thatScrutinee.sym) || continuation.referencesScrutinee(scrutinee) || tail.referencesScrutinee(scrutinee) case Split.Let(_, _, tail) => tail.referencesScrutinee(scrutinee) @@ -115,7 +115,7 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C /** Specialize this split under the given assumption, falling back to the * unchanged split when specialization is a no-op. */ - private def specialized(mode: Mode, scrutinee: Term.Ref, pattern: FlatPattern)(using VarSet): Split = + private def specialized(mode: Mode, scrutinee: Term.SimpleRef, pattern: FlatPattern)(using VarSet): Split = specialize(split, mode, scrutinee, pattern).getOrElse(split) /** Replace all `UseSplit(sym)` references in `split` with a duplicate of `body`. */ @@ -217,7 +217,7 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C private def specialize( split: Split, mode: Mode, - scrutinee: Term.Ref, + scrutinee: Term.SimpleRef, pattern: FlatPattern )(using VarSet): Opt[Split] = trace( pre = s"S$mode <<< ${scrutinee.showDbg} is ${pattern.showDbg} : ${split.prettyPrint}", @@ -549,7 +549,7 @@ class Normalization(lowering: Lowering)(using tl: TL)(using Raise, Ctx, State, C val loopEnd: Path = Select(State.runtimeSymbol.asSimpleRef, Tree.Ident("LoopEnd"))(S(State.loopEndSymbol), N)(false) val blk = blockBuilder - .define(FunDefn(N, f, tSym, PlainParamList(Nil)(N) :: Nil, Begin(body, Return(loopEnd)))(configOverride = N, annotations = Nil)) + .define(FunDefn(N, f, tSym, codegen.LoweredParamList.plain(Nil)(N) :: Nil, Begin(body, Return(loopEnd)))(configOverride = N, annotations = Nil)) .assign(loopResult, Call(f.asMemberRef(tSym), Nil ne_:: Nil)(CallMetadata.mlsFunWithEffect, N)) if summon[LoweringCtx].mayRet then blk diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/TermSynthesizer.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/TermSynthesizer.scala index 6540a74c59..5d85554352 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/TermSynthesizer.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/TermSynthesizer.scala @@ -5,17 +5,30 @@ package ucs import hkmc2.utils.*, shorthands.* import syntax.Tree, Tree.*, Elaborator.{Ctx, State, ctx} -/** This trait includes some helpers for synthesizing `Term`s which look like - * they have already been processed by the `Resolver`. Its methods should only - * be called in stages after the `Resolver`. Currently, its derived classes are - * `Normalization`, `Compiler`, and `SplitCompiler`. */ +/** Build matcher terms after resolution. Nominal references carry their known + * targets; accesses to generated records and pattern protocols use structural + * lookup. These terms are lowered directly, without another resolution pass. */ trait TermSynthesizer(using State): - protected final def sel(p: Term, k: Ident): Term.SynthSel = - (Term.SynthSel(p, k)(N, FlowSymbol.synthSel(k.name), N, N): Term.SynthSel).resolve - protected final def sel(p: Term, k: Ident, s: MemberSymbol): Term.SynthSel = - (Term.SynthSel(p, k)(S(s), FlowSymbol.synthSel(k.name), N, N): Term.SynthSel).resolve - protected final def sel(p: Term, k: Str): Term.SynthSel = sel(p, Ident(k): Ident) - protected final def sel(p: Term, k: Str, s: MemberSymbol): Term.SynthSel = sel(p, Ident(k): Ident, s) + protected final def ref(target: DefinitionSymbol[?]): Term.MemberRef = + val member = target.bms.getOrElse(lastWords("A generated member reference requires a block member")) + val ref = new Term.MemberRef(member)(N, FlowSymbol.memSym(member)) + ref.resolvedTargets = target :: Nil + ref + + /** These fields have runtime names but no nominal member declarations. + * Their receivers are generated records/pattern objects, or strings guarded + * by prefix matching before their length is read. */ + protected final def sel(p: Term, k: Ident): Term.NewSel = + val sel = new Term.NewSel(p, k, N)(FlowSymbol.synthSel(k.name)) + sel.hasDynamicTarget = true + sel + protected final def sel(p: Term, k: Str): Term.NewSel = sel(p, Ident(k): Ident) + protected final def sel(p: Term, target: DefinitionSymbol[?]): Term.NewSel = + val member = target.bms.getOrElse(lastWords("A generated member selection requires a block member")) + val sel = new Term.NewSel(p, Ident(member.nme), N)(FlowSymbol.synthSel(member.nme)) + sel.resolvedTargets = target :: Nil + sel.resolvedMembers = member :: Nil + sel protected final def int(i: Int) = Term.Lit(IntLit(BigInt(i))) protected final def str(s: Str) = Term.Lit(StrLit(s)) protected final def `null` = Term.Lit(UnitLit(true)) @@ -25,7 +38,7 @@ trait TermSynthesizer(using State): protected final def tup(xs: Fld*): Term.Tup = Term.Tup(xs.toList)(DummyTup) protected final def app(l: Term, r: Term, label: Str): Term.App = app(l, r, FlowSymbol(label)) protected final def app(l: Term, r: Term, s: FlowSymbol): Term.App = - (Term.App(l, r)(App(Dummy, Dummy), N, s): Term.App).resolve + Term.App(l, r)(App(Dummy, Dummy), N, s) protected final def `new`(cls: Term, args: Ls[Term], label: Str): Term.New = Term.New(cls, args, N)(FlowSymbol.neww(), N) protected final def rcd(fields: RcdField*): Term.Rcd = Term.Rcd(false, fields.toList) @@ -36,11 +49,11 @@ trait TermSynthesizer(using State): protected final def param = Param.simple protected final def paramList(params: Param*) = PlainParamList(params.toList)(N) - private lazy val runtimeRef: Term.Ref = State.runtimeSymbol.ref().resolve + private lazy val runtimeRef: Term.SimpleRef = Term.SimpleRef(State.runtimeSymbol)(N) /** Make a term that looks like `runtime.MatchSuccess` with its symbol. */ protected lazy val matchSuccessClass = - sel(runtimeRef, "MatchSuccess", State.matchSuccessClsSymbol).resolved(State.matchSuccessClsSymbol) + sel(runtimeRef, State.matchSuccessClsSymbol) /** Make a pattern that looks like `runtime.MatchSuccess.class`. */ protected def matchSuccessPattern(parametersOpt: Opt[Ls[LocalVarSymbol]]): FlatPattern.ClassLike = @@ -49,26 +62,26 @@ trait TermSynthesizer(using State): /** Make a term that looks like `runtime.MatchFailure` with its symbol. */ protected lazy val matchFailureClass = - sel(runtimeRef, "MatchFailure", State.matchFailureClsSymbol).resolved(State.matchFailureClsSymbol) + sel(runtimeRef, State.matchFailureClsSymbol) /** Make a pattern that looks like `runtime.MatchFailure.class`. */ protected def matchFailurePattern(parametersOpt: Opt[Ls[LocalVarSymbol]]): FlatPattern.ClassLike = val parameters = parametersOpt.map(_.map(_ -> N)) FlatPattern.ClassLike(matchFailureClass, State.matchFailureClsSymbol, parameters, false)(Tree.Dummy) - protected lazy val tupleSlice = sel(sel(runtimeRef, "Tuple", State.tupleSymbol), "slice", State.tupleSliceSymbol) - protected lazy val tupleLazySlice = sel(sel(runtimeRef, "Tuple", State.tupleSymbol), "lazySlice", State.tupleLazySliceSymbol) - protected lazy val tupleGet = sel(sel(runtimeRef, "Tuple", State.tupleSymbol), "get", State.tupleGetSymbol) - protected lazy val stringStartsWith = sel(sel(runtimeRef, "Str", State.strSymbol), "startsWith", State.strStartsWithSymbol) - protected lazy val stringGet = sel(sel(runtimeRef, "Str", State.strSymbol), "get", State.strGetSymbol) - protected lazy val stringTake = sel(sel(runtimeRef, "Str", State.strSymbol), "take", State.strTakeSymbol) - protected lazy val stringLeaveOut = sel(sel(runtimeRef, "Str", State.strSymbol), "leave", State.strLeaveOutSymbol) + protected lazy val tupleSlice = sel(sel(runtimeRef, State.tupleSymbol), State.tupleSliceSymbol) + protected lazy val tupleLazySlice = sel(sel(runtimeRef, State.tupleSymbol), State.tupleLazySliceSymbol) + protected lazy val tupleGet = sel(sel(runtimeRef, State.tupleSymbol), State.tupleGetSymbol) + protected lazy val stringStartsWith = sel(sel(runtimeRef, State.strSymbol), State.strStartsWithSymbol) + protected lazy val stringGet = sel(sel(runtimeRef, State.strSymbol), State.strGetSymbol) + protected lazy val stringTake = sel(sel(runtimeRef, State.strSymbol), State.strTakeSymbol) + protected lazy val stringLeaveOut = sel(sel(runtimeRef, State.strSymbol), State.strLeaveOutSymbol) /** Make a term that looks like `runtime.Tuple.get(t, i)`. */ protected final def callTupleGet(t: Term, i: Int, label: Str): Term = callTupleGet(t, i, FlowSymbol(label)) - /** Make a term that looks like `runtime.Tuple.slice(t, i)`. */ + /** Make a term that looks like `runtime.Tuple.get(t, i)`. */ protected final def callTupleGet(t: Term, i: Int, s: FlowSymbol): Term = app(tupleGet, tup(fld(t), fld(int(i))), s) @@ -76,20 +89,20 @@ trait TermSynthesizer(using State): protected final def callTupleSlice(t: Term, i: Int, j: Int, label: Str): Term = app(tupleSlice, tup(fld(t), fld(int(i)), fld(int(j))), label) - /** Make a term that looks like `runtime.Str.startsWith(t, p)`. */ - protected final def callStringStartsWith(t: Term.Ref, p: Term, label: Str) = + /** Make a term that looks like `runtime.StrOps.startsWith(t, p)`. */ + protected final def callStringStartsWith(t: Term.SimpleRef, p: Term, label: Str) = app(stringStartsWith, tup(fld(t), fld(p)), label) - /** Make a term that looks like `runtime.Str.get(t, i)`. */ - protected final def callStringGet(t: Term.Ref, i: Int, label: Str) = + /** Make a term that looks like `runtime.StrOps.get(t, i)`. */ + protected final def callStringGet(t: Term.SimpleRef, i: Int, label: Str) = app(stringGet, tup(fld(t), fld(int(i))), label) - /** Make a term that looks like `runtime.Str.drop(t, n)`. */ - protected final def callStringTake(t: Term.Ref, n: Int, label: Str) = + /** Make a term that looks like `runtime.StrOps.take(t, n)`. */ + protected final def callStringTake(t: Term.SimpleRef, n: Int, label: Str) = app(stringTake, tup(fld(t), fld(int(n))), label) - /** Make a term that looks like `runtime.Str.drop(t, n)`. */ - protected final def callStringDrop(t: Term.Ref, n: Int, label: Str) = + /** Make a term that looks like `runtime.StrOps.leaveOut(t, n)`. */ + protected final def callStringDrop(t: Term.SimpleRef, n: Int, label: Str) = app(stringLeaveOut, tup(fld(t), fld(int(n))), label) protected final def tempLet(dbgName: Str, term: Term)(inner: TempSymbol => Split): Split = @@ -117,7 +130,7 @@ trait TermSynthesizer(using State): /** Make a `Branch` that calls `Pattern` symbols' `unapply` functions. */ def makeLocalPatternBranch( - scrut: => Term.Ref, + scrut: => Term.SimpleRef, localPatternSymbol: LocalVarSymbol, inner: => Split, )(fallback: Split): Split = @@ -126,7 +139,7 @@ trait TermSynthesizer(using State): Branch(resultSymbol.safeRef, matchSuccessPattern(N), inner) ~: fallback protected final def makeTupleBranch( - scrut: => Term.Ref, + scrut: => Term.SimpleRef, subScrutinees: Ls[LocalVarSymbol], consequent: => Split, alternative: Split @@ -139,7 +152,7 @@ trait TermSynthesizer(using State): ) ~: alternative protected final def makeTupleBranch( - scrut: => Term.Ref, + scrut: => Term.SimpleRef, leading: Ls[LocalVarSymbol], spread: LocalVarSymbol, trailing: Ls[LocalVarSymbol], diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/package.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/package.scala index cb54a64b9f..170ece1f9a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/package.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ucs/package.scala @@ -15,12 +15,8 @@ package object ucs: def bug(using Line, FileName, Name, Raise)(msgs: (Message, Option[Loc])*): Unit = raise(InternalError(msgs.toList)) - extension (symbol: Symbol) - /** Create a `Ref` that does not have any implicit arguments. We need this - * function because we generate a lot of `Ref`s after implicit resolution. - * Writing `.resolve` is too verbose. - */ - def safeRef(using Elaborator.State): Term.Ref = symbol.ref().resolve + extension (symbol: SimpleRefSymbol) + def safeRef(using Elaborator.State): Term.SimpleRef = Term.SimpleRef(symbol)(N) extension (op: Keyword.Infix) infix def unapply(tree: Tree): Opt[(Tree, Tree)] = tree match diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Compiler.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Compiler.scala index b276459122..3dac9612ad 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Compiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/Compiler.scala @@ -6,7 +6,7 @@ package ups import hkmc2.utils.*, shorthands.* import syntax.{Keyword, LetBind, Tree}, Tree.{BoolLit, DecLit, Ident, IntLit, StrLit, UnitLit} -import Term.{Blk, Rcd, Ref, SynthIf, SynthSel} +import Term.{Blk, Rcd, SynthIf} import Pattern.{Instantiation, Head} import Elaborator.{Ctx, State, ctx}, utils.TL import ucs.{TermSynthesizer, FlatPattern, safeRef} @@ -575,15 +575,7 @@ object Compiler: * matcher code, preserving the path chosen by source resolution. */ def preservedReference(term: Term)(using State, codegen.Erasure): Term = - /** To make `Lowering` happy about the terms. */ - def fillImplicitArgs(term: Term): Term = term match - case ref: Ref => - if ref.expansion.isDefined then ref else ref.resolve - case sel: SynthSel => - fillImplicitArgs(sel.prefix) - if sel.expansion.isDefined then sel else sel.resolve - case _: Term => term - fillImplicitArgs(term.mkClone) + term.mkClone import Pattern.* diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/FixedPointCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/FixedPointCompiler.scala index 03ec87f1ec..9d1d7db92a 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/FixedPointCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/FixedPointCompiler.scala @@ -650,7 +650,7 @@ class FixedPointCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, app(matcher.safeRef, tup(fld(argument)), label) private def refEq(left: Term, right: Term): Term = - app(State.builtinOpsMap("===").ref(Ident("===")), tup(fld(left), fld(right)), "reference equality") + app(State.builtinOpsMap("===").safeRef, tup(fld(left), fld(right)), "reference equality") /** Run `make` with a thunk producing the `rest` split. When the thunk is * invoked more than once, `rest` is bound as a join point and every diff --git a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/SplitCompiler.scala b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/SplitCompiler.scala index 74b4ff6b22..ae72795c73 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/SplitCompiler.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/semantics/ups/SplitCompiler.scala @@ -10,11 +10,11 @@ import collection.mutable.{Buffer, HashMap}, collection.immutable.SeqMap import codegen.{ErasedType, PrimitiveType} import Elaborator.{Ctx, State, ctx}, utils.TL import semantics.Pattern as SP // "SP" is short for "semantic patterns" -import Term.Ref +import Term.SimpleRef import ups.Compiler.ResultMode object SplitCompiler: - /** A class that can generate `Ref` to the scrutinee. It also comes with a few + /** A class that can generate `SimpleRef` to the scrutinee. It also comes with a few * mutable maps to reuse sub-scrutinees. Memoization of sub-scrutinees help * the normalization to merge let bindings from different branches. */ @@ -24,7 +24,7 @@ object SplitCompiler: private val tupleLead: HashMap[Int, SymbolScrut] = HashMap.empty private val tupleLast: HashMap[Int, SymbolScrut] = HashMap.empty - def apply()(using State): Term.Ref + def apply()(using State): SimpleRef def getSubScrutinee(cs: ClassSymbol | PatternSymbol)(i: Int)(using State): SymbolScrut = val scrutinees = subScrutinees.getOrElseUpdate(cs, Buffer.empty) @@ -38,14 +38,8 @@ object SplitCompiler: def getFieldScrutinee(fieldName: Ident)(using State): SymbolScrut = fields.getOrElseUpdate(fieldName, TempSymbol(N, erasedType = N, s"field_${fieldName.name}$$").toScrut) - object Scrut: - def from(ref: Term.Ref): RefScrut = RefScrut(() => ref) - - class RefScrut(make: () => Term.Ref) extends Scrut: - def apply()(using State): Ref = make() - class SymbolScrut(val symbol: LocalVarSymbol) extends Scrut: - def apply()(using State): Ref = symbol.ref() + def apply()(using State): SimpleRef = SimpleRef(symbol)(N) extension (symbol: LocalVarSymbol) def toScrut: SymbolScrut = SymbolScrut(symbol) @@ -111,7 +105,7 @@ object SplitCompiler: private lazy val symbol = _hasBeenUsed = true TempSymbol(N, erasedType = N, nameHint.getOrElse("output")) - def apply()(using State): Ref = symbol.safeRef + def apply()(using State): SimpleRef = SimpleRef(symbol)(N) def toList: Ls[TempSymbol] = if _hasBeenUsed then symbol :: Nil else Nil def toLet(term: => Term, tail: Split): Split = if _hasBeenUsed then Split.Let(symbol, term, tail) else tail @@ -582,7 +576,7 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise val wrapPatternArgumentDefinitions = buildPatternArguments(patternBindings, _) // Then, we can call the `unapply` method. val unapplyArguments = patternBindings.map(_._1.safeRef |> fld) :+ fld(scrutinee()) - val unapplyCall = app(sel(patternTerm, "unapply").resolve, tup(unapplyArguments*), s"result of unapply") + val unapplyCall = app(sel(patternTerm, "unapply"), tup(unapplyArguments*), s"result of unapply") val unapplyResult = TempSymbol(N, erasedType = N, "unapplyResult") val wrapUnapply = Split.Let(unapplyResult, unapplyCall, _) // Then, we need to destruct the produced `MatchSuccess`. @@ -653,7 +647,7 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise validatePatternParameter[MakeSplit]( parameterTerm, parameterSymbol, arguments, patternLoc, RejectSplit ): (makeConsequent, alternative) => - val unapplyTerm = sel(parameterTerm, "unapply").resolve + val unapplyTerm = sel(parameterTerm, "unapply") val unapplyCall = app(unapplyTerm, tup(fld(scrutinee())), s"result of unapply") tempLet(s"matchSuccess_${parameterSymbol.name}", unapplyCall): matchSuccessSymbol => val outputSymbol = TempSymbol(N, erasedType = N, "output").toScrut @@ -752,7 +746,7 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise if outputNeeded then val combinedOutput = new LazyScrut(S("concatenatedOutput")) val combinedTerm = app( - add.ref(), + add.safeRef, tup(fld(consumedOutput()), fld(postfixOutput())), "concatenated string output" ) @@ -883,7 +877,7 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise makeMatchSplit(scrutinee, pattern, true)( (output, bindings) => val guardSymbol = TempSymbol(N, erasedType = N, "guardResult") - val branch = Branch(guardSymbol.ref(), makeConsequent(output, bindings)) + val branch = Branch(guardSymbol.safeRef, makeConsequent(output, bindings)) val innermost = Split.Let(guardSymbol, guard, branch ~: Split.End) // The creation of bindings here is repeated with the creation of // bindings during desugaring. We can add a piece of information @@ -937,7 +931,7 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise validatePatternParameter[MakePrefixSplit]( parameterTerm, parameterSymbol, arguments, patternLoc, RejectPrefixSplit ): (makeConsequent, alternative) => - val unapplyTerm = sel(parameterTerm, "unapplyStringPrefix").resolve + val unapplyTerm = sel(parameterTerm, "unapplyStringPrefix") val unapplyCall = app(unapplyTerm, tup(fld(scrutinee())), s"result of unapply") tempLet(s"matchSuccess_${parameterSymbol.name}", unapplyCall): matchSuccessSymbol => // Destruct the `MatchSuccess` produced by the `unapply` method. @@ -959,6 +953,18 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise protected def makeStringPrefixMatchSplit( scrutinee: Scrut, pattern: SP, + )(using Raise): MakePrefixSplit = + val make = makeStringPrefixMatchSplitImpl(scrutinee, pattern) + // Prefix patterns also receive non-strings when an outer match tries + // different token kinds. Refine the input before reading its length or + // invoking string helpers; a missing property is not a failed match. + make.whenAccept: (makeConsequent, alternative) => + Branch(scrutinee(), FlatPattern.ClassLike(ref(ctx.builtins.Str), ctx.builtins.Str, N, false)(Tree.Dummy), + make(makeConsequent, alternative)) ~: alternative + + private def makeStringPrefixMatchSplitImpl( + scrutinee: Scrut, + pattern: SP, )(using Raise): MakePrefixSplit = pattern match case ctor @ Constructor(target, arguments) => (if newResolution then resolvedConstructor(ctor) else target.resolvedSym) match @@ -1031,11 +1037,8 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise // Because the wildcard pattern always matches, we can match the entire // string and returns an empty string as the remaining value. val emptyStringSymbol = TempSymbol(N, erasedType = S(ErasedType.Str), "emptyString") - makeConsequent(scrutinee, emptyStringSymbol.toScrut, SeqMap.empty) - Branch(scrutinee(), FlatPattern.ClassLike(ctx.builtins.Str.safeRef, ctx.builtins.Str, N, false)(Tree.Dummy), - Split.Let(emptyStringSymbol, str(""), - makeConsequent(scrutinee, emptyStringSymbol.toScrut, SeqMap.empty)) - ) ~: alternative + Split.Let(emptyStringSymbol, str(""), + makeConsequent(scrutinee, emptyStringSymbol.toScrut, SeqMap.empty)) case Literal(prefix: StrLit) => (makeConsequent, alternative) => // Check if the scrutinee is the same as the literal. If so, we return // an empty string as the remaining value. @@ -1076,7 +1079,7 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise (rightConsumedOutput, rightRemainingOutput, rightBindings) => val combinedOutputSymbol = LazyScrut(S("combinedOutput")) combinedOutputSymbol.toLet( - app(add.ref(), tup(fld(leftConsumedOutput()), fld(rightConsumedOutput())), "combined output"), + app(add.safeRef, tup(fld(leftConsumedOutput()), fld(rightConsumedOutput())), "combined output"), makeConsequent(combinedOutputSymbol, rightRemainingOutput, leftBindings ++ rightBindings)), alternative), alternative) @@ -1136,7 +1139,7 @@ class SplitCompiler(using codegen.Erasure)(using tl: TL)(using State, Ctx, Raise (makeConsequent, alternative) => make( (output, remains, bindings) => val guardSymbol = TempSymbol(N, erasedType = N, "guardResult") - val branch = Branch(guardSymbol.ref(), makeConsequent(output, remains, bindings)) + val branch = Branch(guardSymbol.safeRef, makeConsequent(output, remains, bindings)) val innermost = Split.Let(guardSymbol, guard, branch ~: Split.End) // The creation of bindings here is repeated with the creation of // bindings during desugaring. We can add a piece of information diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala index 604129221f..c889b39bc6 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/ParseRule.scala @@ -425,7 +425,7 @@ class ParseRules(using State): singleKw(`true`)(BoolLit(true)), singleKw(`false`)(BoolLit(false)), singleKw(`dyn`)(Keywrd(`dyn`)), - singleKw(`undefined`)(UnitLit(false)), + singleKw(`undefined`)(Keywrd(`undefined`)), singleKw(`null`)(UnitLit(true)), singleKw(`this`)(Keywrd(Keyword.`this`)), singleKw(`super`)(Keywrd(Keyword.`super`)), diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala index a0d4fcfff7..af66e4f230 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Parser.scala @@ -902,6 +902,8 @@ abstract class Parser( * which is `Int.MinValue` (see `Keyword.rightPrecOrMin`). A sentinel like -1 would then make the body * `{ fun f: Int }` be parsed as an operator split instead of as a refinement of `A`. */ private def opSplitLeftPrec(tok: Stroken): Int = tok match + // An annotation may be the first member of a class/record body. + case IDENT("@", _) => Int.MinValue case SELECT(_, _) => SelPrec case id @ (IDENT(_, true) | KEYWORD(_: Keyword.InfixSplittable)) => id match case KEYWORD(Keyword.`of`) => AppPrec @@ -1096,6 +1098,30 @@ abstract class Parser( consume consume exprCont(MemberProj(acc, new Ident(id).withLoc(S(l1))).withLoc(S(l0 ++ l1)), prec, allowNewlines = allowNewlines) + + // * Postfix `?` marks an optional parameter, as in `fun f(x, y?)` or `(Int, Int?) -> Int`. + // * The marker applies to the whole parameter (so `Int | Str?` means `(Int | Str)?`), + // * so it is only recognized below the precedence of `=` (i.e., not within operator operands), + // * and only when it ends the element. In `y?: Int`, the lexer merges `?` and `:` into a single + // * `?:` operator token, which we split back into the marker followed by the `:` keyword. + // * Other operators starting with `?`, such as `??` and `?>`, are distinct operator tokens. + // * The marker must be attached to the parameter, without intervening whitespace: otherwise, + // * `?` and `?:` are ordinary operators, so that `a ?: b` applies a user-defined `?:` operator, + // * while `y ?` and `y ?: Int` are not optional parameters. + case (OP("?"), l0) :: rest + if prec < Keyword.eqPrec.get && acc.toLoc.exists(_.spanEnd === l0.spanStart) + && NOISE.get(rest).match + case Nil | (_: NEWLINE_COMMA, _) :: _ => true + case _ => false + => + consume + exprCont(Optional(acc).withLoc(acc.toLoc.fold(S(l0))(l => S(l ++ l0))), prec, allowNewlines = allowNewlines) + case (OP("?:"), l0) :: _ if prec < Keyword.eqPrec.get && acc.toLoc.exists(_.spanEnd === l0.spanStart) => + consume + val markerLoc = Loc(l0.spanStart, l0.spanStart + 1, l0.origin) + cur = (IDENT(":", true), Loc(l0.spanStart + 1, l0.spanEnd, l0.origin)) :: cur + exprCont(Optional(acc).withLoc(acc.toLoc.fold(S(markerLoc))(l => S(l ++ markerLoc))), prec, allowNewlines = allowNewlines) + case (OP(opStr), l0) :: _ if /* isInfix(opStr) && */ opPrec(opStr)._1 > prec => consume val v = Ident(opStr).withLoc(S(l0)) diff --git a/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala b/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala index 32472b6e32..0615c14a16 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/syntax/Tree.scala @@ -109,6 +109,8 @@ enum Tree extends AutoLocated: case Effectful(eff: Tree, body: Tree) case Outer(name: Opt[Tree]) case Spread(kw: Keywrd[Keyword.Ellipsis], body: Opt[Tree]) + /** A parameter or function type parameter marked optional with a postfix `?`, as in `fun f(x, y?)`. */ + case Optional(body: Tree) case Annotated(annotation: Tree, target: Tree) case Directive(prefix: Tree, body: Tree) case Constructor(decl: Tree) @@ -166,6 +168,7 @@ enum Tree extends AutoLocated: case OpenIn(opened, body) => Vector.double(opened, body) case Def(lhs, rhs) => Vector.double(lhs, rhs) case Spread(kw, body) => Vector.single(kw) ++ body.toVector + case Optional(body) => Vector.single(body) case Annotated(annotation, target) => Vector.double(annotation, target) case Directive(prefix, body) => Vector.double(prefix, body) case Constructor(decl) => Vector.single(decl) @@ -221,6 +224,7 @@ enum Tree extends AutoLocated: case Hndl(_, _, _, _) => "handle" case Def(lhs, rhs) => "defining assignment" case Spread(_, _) => "spread" + case Optional(_) => "optional parameter" case Annotated(_, _) => "annotated" case Directive(_, _) => "directive" case Open(_) => "open" @@ -340,8 +344,8 @@ enum Tree extends AutoLocated: LetLike(kw, Ident("_").withLocOf(und), r, b) // `open` keeps its existing expression grammar; a declaration operand makes it a modifier. - case Open(body: (TypeOrTermDef | Modified | Annotated)) => - Annotated(Keywrd(Keyword.`open`).withLocOf(this), body.desugared) + case PossiblyAnnotated(anns, opened @ Open(body: (TypeOrTermDef | Modified | Annotated))) => + PossiblyAnnotated(anns, Annotated(Keywrd(Keyword.`open`).withLocOf(opened), body.desugared)) case PossiblyAnnotated(anns, m: Modified) => PossiblyAnnotated(anns, m match @@ -424,6 +428,18 @@ enum Tree extends AutoLocated: // fun f(using <...>) case TermDef(Ins, inner, N) => go(inner, flags, modifiers + Ins) + // fun f(a?) + case Optional(inner) => + go(inner, flags.copy(opt = true), modifiers) + // fun f(a?: A) + case InfixApp(Optional(inner), kw @ Keywrd(Keyword.`:`), sign) => + go(InfixApp(inner, kw, sign), flags.copy(opt = true), modifiers) + // fun f(...a?), which is reported during elaboration + case Spread(kw, S(Optional(inner))) => + go(Spread(kw, S(inner)), flags.copy(opt = true), modifiers) + // fun f(drop a) + case PrefixApp(Keywrd(Keyword.`drop`), inner) => + go(inner, flags.copy(droppable = true), modifiers) // * Base Case (for `using` clause) // fun f(using A) diff --git a/hkmc2/shared/src/main/scala/hkmc2/utils/document/package.scala b/hkmc2/shared/src/main/scala/hkmc2/utils/document/package.scala index 71695fc461..7c7117fc1c 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/utils/document/package.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/utils/document/package.scala @@ -31,10 +31,12 @@ package object document: // (ls map lift).mkDocument() // } - implicit class LiftableSeqOps[A: Liftable](docs: Seq[A]): - def mkDocument(pre: Document, sep: Document, post: Document): Document = docs filter (_ != empty) match - case d +: ds => pre :: ((d: Document) +: ds.map(sep :: _)).foldLeft(empty)(_ :: _) :: post - case _ => pre :: post + implicit class LiftableSeqOps[A: Liftable](docs: IterableOnce[A]): + def mkDocument(pre: Document, sep: Document, post: Document): Document = + val ds = docs.iterator.filter(_ != empty) + ds.nextOption() match + case Some(d) => pre :: ds.map(sep :: _).foldLeft(d: Document)(_ :: _) :: post + case _ => pre :: post def mkDocument(sep: Document = empty): Document = mkDocument(empty, sep, empty) diff --git a/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala b/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala index a29402f9c2..fe4c156339 100644 --- a/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala +++ b/hkmc2/shared/src/main/scala/hkmc2/utils/utils.scala @@ -120,12 +120,14 @@ class DebugPrinter( if isMethod then flags += "method" if hasResultAnnotation then flags += "result annotation" flags.mkString("(", ", ", ")") - case FldFlags(mut, spec, pat, value) => + case FldFlags(mut, spec, pat, value, opt, droppable) => val flags = Buffer.empty[Str] if mut then flags += "mut" if spec then flags += "spec" if pat then flags += "pat" if value then flags += "val" + if opt then flags += "opt" + if droppable then flags += "drop" flags.mkString("(", ", ", ")") case ParamListFlags(ctx) => val flags = Buffer.empty[Str] diff --git a/hkmc2/shared/src/test/mlscript-compile/Block.mls b/hkmc2/shared/src/test/mlscript-compile/Block.mls index 67c91d5e84..7eb514ad09 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Block.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Block.mls @@ -15,7 +15,7 @@ type Opt[A] = Option[A] // dependancies referenced in Block classes, referencing implementation in Term.mls -type Literal = null | undefined | Str | Int | Num | Bool +type Literal = null | unsafe.undefined | Str | Int | Num | Bool type ParamList = Array[Symbol] @@ -99,7 +99,7 @@ fun indent(s: Str) = s.replaceAll("\n", "\n ") fun showLiteral(l: Literal) = if l is - undefined then "undefined" + unsafe.undefined then "undefined" null then "null" Str then "\"" + l.toString() + "\"" else l.toString() diff --git a/hkmc2/shared/src/test/mlscript-compile/FingerTreeList.mls b/hkmc2/shared/src/test/mlscript-compile/FingerTreeList.mls index d354e00be8..05bb2840f2 100644 --- a/hkmc2/shared/src/test/mlscript-compile/FingerTreeList.mls +++ b/hkmc2/shared/src/test/mlscript-compile/FingerTreeList.mls @@ -32,7 +32,7 @@ object Nil data class View[T](e1: T, rest: FingerTree[T]) class FingerTree[T] extends IterableBase with - override fun iterator() = + @annotations.jsSymbol(Symbol.iterator) override fun iterator() = let current = this IteratorImpl of () => if popFront(current) is Nil then Result.Done diff --git a/hkmc2/shared/src/test/mlscript-compile/FingerTreeList_StressTest.mls b/hkmc2/shared/src/test/mlscript-compile/FingerTreeList_StressTest.mls index ed3c817f9d..3217f734b5 100644 --- a/hkmc2/shared/src/test/mlscript-compile/FingerTreeList_StressTest.mls +++ b/hkmc2/shared/src/test/mlscript-compile/FingerTreeList_StressTest.mls @@ -39,7 +39,7 @@ object Nil data class View[T](e1: T, rest: FingerTree[T]) class FingerTree[T] extends IterableBase with - override fun iterator() = + @annotations.jsSymbol(Symbol.iterator) override fun iterator() = let current = this IteratorImpl of () => if popFront(current) is Nil then Result.Done diff --git a/hkmc2/shared/src/test/mlscript-compile/Iter.mls b/hkmc2/shared/src/test/mlscript-compile/Iter.mls index 9c05a4f855..dbba74ee32 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Iter.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Iter.mls @@ -5,6 +5,7 @@ import "./Option.mls" import "./Stack.mls" +open annotations { jsSymbol } open Option { Some, None } open Stack { Nil, Cons } @@ -12,8 +13,7 @@ open Stack { Nil, Cons } module Iter with ... open class IterableBase with - set this.(Symbol.iterator) = iterator - open fun iterator() = throw new Error("Not implemented: iterator()") + @jsSymbol(Symbol.iterator) open fun iterator() = throw new Error("Not implemented: iterator()") // require that subclasses are immutable fun toArray(): Array = [...this] @@ -74,11 +74,12 @@ open class IterableBase with // * Implements the `IterableImpl` protocol. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterable_protocol class IterableImpl(mk: () -> Iterator) extends IterableBase with - override fun iterator() = mk() + @jsSymbol(Symbol.iterator) override fun iterator() = mk() // * Implements the `Iterator` protocol. // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols#the_iterator_protocol -class IteratorImpl(val next: Function) +class IteratorImpl(val next: Function) with + @jsSymbol(Symbol.iterator) fun iterator() = this module Result with class Next(val value) with @@ -91,9 +92,9 @@ module Result with // indicate the object's class in the error message, which is useful for debugging. fun getIterator(something): Iterator = let test = something.(Symbol.iterator) - if test is undefined then + if test is unsafe.undefined then throw TypeError of "Not an iterable: " + if - something is undefined then "undefined" + something is unsafe.undefined then "undefined" something is null then "null" let prototype = Reflect.getPrototypeOf of something prototype is null then "object" diff --git a/hkmc2/shared/src/test/mlscript-compile/LazyArray.mls b/hkmc2/shared/src/test/mlscript-compile/LazyArray.mls index 6350913f63..13c4034583 100644 --- a/hkmc2/shared/src/test/mlscript-compile/LazyArray.mls +++ b/hkmc2/shared/src/test/mlscript-compile/LazyArray.mls @@ -61,7 +61,7 @@ class LazyArr extends IterableBase with // Note: Ideally `underlying` should be typed as `IterableBase`, but `Array` currently does not extend it. class View(val underlying: Indexable, val start: Int, val end: Int) extends LazyArr with - override fun iterator() = + @annotations.jsSymbol(Symbol.iterator) override fun iterator() = let idx = start let until = underlying.length - end IteratorImpl of () => @@ -87,7 +87,7 @@ object SpliceMarker class Splice(val bits: Array, val len: Int) extends LazyArr with let materialized = null - override fun iterator() = + @annotations.jsSymbol(Symbol.iterator) override fun iterator() = let idx = 0 let uitr = null diff --git a/hkmc2/shared/src/test/mlscript-compile/LazyFingerTree.mls b/hkmc2/shared/src/test/mlscript-compile/LazyFingerTree.mls index 837871d5dd..4b3f636fc9 100644 --- a/hkmc2/shared/src/test/mlscript-compile/LazyFingerTree.mls +++ b/hkmc2/shared/src/test/mlscript-compile/LazyFingerTree.mls @@ -58,7 +58,7 @@ class LazyFT extends IterableBase with class View(val underlying: Indexable, val start: Int, val end: Int) extends LazyFT with - override fun iterator() = + @annotations.jsSymbol(Symbol.iterator) override fun iterator() = let idx = start let until = underlying.length - end IteratorImpl of () => @@ -84,7 +84,7 @@ object SpliceMarker class Splice(val bits: Array, val len: Int) extends LazyFT with let materialized = null - override fun iterator() = materialize().iterator() + @annotations.jsSymbol(Symbol.iterator) override fun iterator() = materialize().iterator() fun reify = let acc = FingerTreeList.mk() diff --git a/hkmc2/shared/src/test/mlscript-compile/LegacyGenericLibrary.mls b/hkmc2/shared/src/test/mlscript-compile/LegacyGenericLibrary.mls deleted file mode 100644 index 0a0ddc6509..0000000000 --- a/hkmc2/shared/src/test/mlscript-compile/LegacyGenericLibrary.mls +++ /dev/null @@ -1,7 +0,0 @@ -#lang(0.2.x) - -// Kept in legacy mode to exercise signatures imported by new resolution. -module LegacyGenericLibrary with - class Cell[A](val value: A) with - fun get: A = value - fun getAt(index: Int): A = value diff --git a/hkmc2/shared/src/test/mlscript-compile/NonLocalReturnLibrary.mls b/hkmc2/shared/src/test/mlscript-compile/NonLocalReturnLibrary.mls new file mode 100644 index 0000000000..ddb7d32206 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-compile/NonLocalReturnLibrary.mls @@ -0,0 +1,8 @@ +#lang(0.3.x, strictResolution: true) +#config(effectHandlers: Some(EffectHandlers()), liftDefns: Some(LiftDefns)) + +module NonLocalReturnLibrary with + fun result[A](value: A) = + let escape = => return [value] + escape() + throw 0 diff --git a/hkmc2/shared/src/test/mlscript-compile/Predef.mls b/hkmc2/shared/src/test/mlscript-compile/Predef.mls index 6b138c93ff..f410ce8f74 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Predef.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Predef.mls @@ -74,11 +74,11 @@ type (>:>) Sup[A, B] = Sub[B, A] fun (==) equals(a: Any, b: Any): Bool = if a === b then true a is Array as a and b is Array as b and a.length === b.length then a.every((a, i, ...) => a == b.at(i)) - else a !== undefined and a !== null and b !== undefined and b !== null and + else a !== unsafe.undefined and a !== null and b !== unsafe.undefined and b !== null and let ac = a!constructor - ac !== undefined and ac === b!constructor and + ac !== unsafe.undefined and ac === b!constructor and let md = ac!(Symbols.definitionMetadata) - md !== undefined and + md !== unsafe.undefined and md.2.every(field => field !== null and a!(field) == b!(field)) fun (!=) nequals(a: Any, b: Any): Bool = not a == b diff --git a/hkmc2/shared/src/test/mlscript-compile/Record.mls b/hkmc2/shared/src/test/mlscript-compile/Record.mls index dc27aa273a..0ad909479e 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Record.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Record.mls @@ -10,7 +10,7 @@ import "./Iter.mls" module Record with... fun steal(from, ...fields) = - let rcd = new mut Object + let rcd = new mut Object() fields Iter.each of f => set rcd.(f) = from.(f) rcd diff --git a/hkmc2/shared/src/test/mlscript-compile/Rendering.mls b/hkmc2/shared/src/test/mlscript-compile/Rendering.mls index 058fac6ee7..4159e26d3b 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Rendering.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Rendering.mls @@ -62,7 +62,7 @@ let identifierPattern = new RegExp of private fun noNeedForQuotes(id) = identifierPattern.test(id) // Use this object instead when no options object is given. -let emptyOptions = new Object +let emptyOptions = new Object() let DEFAULT_BREAK_LENGTH = 80 @@ -263,8 +263,8 @@ fun render(target, ...args): Str = [keyStr, valueStr] (get: getter, "set": setter) then let valueStr = if - setter is undefined then "[Getter]" - getter is undefined then "[Setter]" + setter is unsafe.undefined then "[Getter]" + getter is unsafe.undefined then "[Setter]" else "[Getter/Setter]" [keyStr, valueStr] else [keyStr, "‹Non-data property›"] // LP: not sure what to do (thsi was proposed by Copilot...) @@ -350,12 +350,12 @@ fun render(target, ...args): Str = else layoutMultiLine() fun renderValue(arg: Any, level: Int, keyLength: Int, startPos: Int): Str = if arg is // Primitive values - undefined then "undefined" + unsafe.undefined then "undefined" null then "null" Str then JSON.stringify(arg) BigInt as arg then arg.toString() + "n" Symbol as arg and - arg.description is undefined then "Symbol()" + arg.description is unsafe.undefined then "Symbol()" else "Symbol(\"" + arg.description + "\")" Num as arg then arg.toString() RegExp as arg then arg.toString() diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs index 539d071b2c..e90d20c54a 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mjs @@ -5,7 +5,7 @@ import RuntimeJS from "./RuntimeJS.mjs"; import Rendering from "./Rendering.mjs"; import LazyArray from "./LazyArray.mjs"; import Iter from "./Iter.mjs"; -let continuation, Runtime1, lambda, lambda1, lambda2, lambda3, lambda4, lambda5, lambda$, lambda$1, Capture$scope301, lambda$2, Capture$scope321, lambda$3, lambda$4, continuation$; +let continuation, Runtime1, lambda, lambda1, lambda2, lambda3, lambda4, lambda5, lambda$, lambda$1, Capture$scope311, lambda$2, Capture$scope331, lambda$3, lambda$4, continuation$; continuation$ = function continuation$(Runtime2, resume) { return (value) => { return continuation(Runtime2, resume, value) @@ -33,54 +33,54 @@ lambda3 = (undefined, function (Runtime2, promise, resume) { continuation$here = continuation$(Runtime2, resume); return runtime.safeCall(promise.then(continuation$here)) }); -(class Capture$scope32 { +(class Capture$scope33 { static { - Capture$scope321 = this + Capture$scope331 = this } constructor(result$0) { this.result$0 = result$0; } toString() { return runtime.render(this); } - static [definitionMetadata] = ["class", "Capture$scope32"]; + static [definitionMetadata] = ["class", "Capture$scope33"]; }); -lambda$3 = (undefined, function (scope32$cap, cont) { +lambda$3 = (undefined, function (scope33$cap, cont) { return (m, marker, ..._) => { - return lambda2(scope32$cap, cont, m, marker, ..._) + return lambda2(scope33$cap, cont, m, marker, ..._) } }); -lambda2 = (undefined, function (scope32$cap, cont, m, marker, ..._) { +lambda2 = (undefined, function (scope33$cap, cont, m, marker, ..._) { let scrut, tmp, tmp1; scrut = runtime.safeCall(m.has(cont)); if (scrut === true) { tmp = ", " + marker; - tmp1 = scope32$cap.result$0 + tmp; - scope32$cap.result$0 = tmp1; + tmp1 = scope33$cap.result$0 + tmp; + scope33$cap.result$0 = tmp1; return runtime.Unit } return runtime.Unit; }); -(class Capture$scope30 { +(class Capture$scope31 { static { - Capture$scope301 = this + Capture$scope311 = this } constructor(result$0) { this.result$0 = result$0; } toString() { return runtime.render(this); } - static [definitionMetadata] = ["class", "Capture$scope30"]; + static [definitionMetadata] = ["class", "Capture$scope31"]; }); -lambda$2 = (undefined, function (scope30$cap, cont) { +lambda$2 = (undefined, function (scope31$cap, cont) { return (m, marker, ..._) => { - return lambda1(scope30$cap, cont, m, marker, ..._) + return lambda1(scope31$cap, cont, m, marker, ..._) } }); -lambda1 = (undefined, function (scope30$cap, cont, m, marker, ..._) { +lambda1 = (undefined, function (scope31$cap, cont, m, marker, ..._) { let scrut, tmp, tmp1; scrut = runtime.safeCall(m.has(cont)); if (scrut === true) { tmp = ", " + marker; - tmp1 = scope30$cap.result$0 + tmp; - scope30$cap.result$0 = tmp1; + tmp1 = scope31$cap.result$0 + tmp; + scope31$cap.result$0 = tmp1; return runtime.Unit } return runtime.Unit; @@ -667,50 +667,39 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { tmp3 = tmp2 + ")"; throw runtime.safeCall(globalThis.Error(tmp3)) } - static checkArgs(functionName, expected, isUB, got) { - let scrut, scrut1, tmp, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, tmp8, tmp9, tmp10, tmp11, tmp12, tmp13, tmp14; - tmp = got < expected; - if (tmp === false) { - if (isUB === true) { - tmp2 = got > expected; - } else { - tmp2 = false; - } - tmp1 = tmp2; - } else { - tmp1 = true; - } - if (tmp1 === true) { - scrut = functionName.length > 0; - if (scrut === true) { - tmp3 = " '" + functionName; - tmp4 = tmp3 + "'"; - } else { - tmp4 = ""; - } - tmp5 = "Function" + tmp4; - tmp6 = tmp5 + " expected "; - if (isUB === true) { - tmp7 = ""; - } else { - tmp7 = "at least "; - } - tmp8 = tmp6 + tmp7; - tmp9 = tmp8 + expected; - tmp10 = tmp9 + " argument"; - scrut1 = expected === 1; + static checkArgs(functionName, expected, got) { + let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, tmp4, tmp5, tmp6, tmp7, tmp8, tmp9; + scrut = got < expected; + if (scrut === true) { + scrut1 = functionName.length > 0; if (scrut1 === true) { - tmp11 = ""; + tmp = " '" + functionName; + tmp1 = tmp + "'"; } else { - tmp11 = "s"; + tmp1 = ""; + } + tmp2 = "Function" + tmp1; + tmp3 = tmp2 + " expected at least "; + tmp4 = tmp3 + expected; + tmp5 = tmp4 + " argument"; + scrut2 = expected === 1; + if (scrut2 === true) { + tmp6 = ""; + } else { + tmp6 = "s"; } - tmp12 = tmp10 + tmp11; - tmp13 = tmp12 + " but got "; - tmp14 = tmp13 + got; - throw runtime.safeCall(globalThis.Error(tmp14)) + tmp7 = tmp5 + tmp6; + tmp8 = tmp7 + " but got "; + tmp9 = tmp8 + got; + throw runtime.safeCall(globalThis.Error(tmp9)) } return runtime.Unit; } + static checkVariadicArgs(functionName, expected, leading, args) { + let tmp; + tmp = leading + args.length; + return Runtime.checkArgs(functionName, expected, tmp) + } static checkSelect(sel, nme, qual) { let scrut, tmp, tmp1, tmp2; scrut = sel === undefined; @@ -872,12 +861,12 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { return header; } static showFunctionContChain(cont, hl, vis, reps) { - let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, tmp4, scope30$cap; - scope30$cap = new Capture$scope301(undefined); + let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, tmp4, scope31$cap; + scope31$cap = new Capture$scope311(undefined); if (cont instanceof Runtime.FunctionContFrame.class) { tmp = cont["constructor"]["name"] + "(pc="; - scope30$cap.result$0 = tmp + cont.saved.at(1); - runtime.safeCall(hl.forEach(lambda$2(scope30$cap, cont))); + scope31$cap.result$0 = tmp + cont.saved.at(1); + runtime.safeCall(hl.forEach(lambda$2(scope31$cap, cont))); scrut = runtime.safeCall(vis.has(cont)); if (scrut === true) { tmp1 = reps + 1; @@ -886,12 +875,12 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { if (scrut1 === true) { throw runtime.safeCall(globalThis.Error("10 repeated continuation frame (loop?)")) } - tmp2 = scope30$cap.result$0 + ", REPEAT"; - scope30$cap.result$0 = tmp2; + tmp2 = scope31$cap.result$0 + ", REPEAT"; + scope31$cap.result$0 = tmp2; } else { runtime.safeCall(vis.add(cont)); } - tmp3 = scope30$cap.result$0 + ") -> "; + tmp3 = scope31$cap.result$0 + ") -> "; tmp4 = Runtime.showFunctionContChain(cont.next, hl, vis, reps); return tmp3 + tmp4 } @@ -902,11 +891,11 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { return "(NOT CONT)"; } static showHandlerContChain(cont, hl, vis, reps) { - let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, scope32$cap; - scope32$cap = new Capture$scope321(undefined); + let scrut, scrut1, scrut2, tmp, tmp1, tmp2, tmp3, scope33$cap; + scope33$cap = new Capture$scope331(undefined); if (cont instanceof Runtime.HandlerContFrame.class) { - scope32$cap.result$0 = cont.handler["constructor"]["name"]; - runtime.safeCall(hl.forEach(lambda$3(scope32$cap, cont))); + scope33$cap.result$0 = cont.handler["constructor"]["name"]; + runtime.safeCall(hl.forEach(lambda$3(scope33$cap, cont))); scrut = runtime.safeCall(vis.has(cont)); if (scrut === true) { tmp = reps + 1; @@ -915,12 +904,12 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { if (scrut1 === true) { throw runtime.safeCall(globalThis.Error("10 repeated continuation frame (loop?)")) } - tmp1 = scope32$cap.result$0 + ", REPEAT"; - scope32$cap.result$0 = tmp1; + tmp1 = scope33$cap.result$0 + ", REPEAT"; + scope33$cap.result$0 = tmp1; } else { runtime.safeCall(vis.add(cont)); } - tmp2 = scope32$cap.result$0 + " -> "; + tmp2 = scope33$cap.result$0 + " -> "; tmp3 = Runtime.showFunctionContChain(cont.next, hl, vis, reps); return tmp2 + tmp3 } @@ -932,15 +921,15 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { } static debugCont(cont) { let tmp, tmp1, tmp2; - tmp = globalThis.Object.freeze(new globalThis.Map()); - tmp1 = globalThis.Object.freeze(new globalThis.Set()); + tmp = globalThis.Object.freeze(new globalThis.Map(undefined)); + tmp1 = globalThis.Object.freeze(new globalThis.Set(undefined)); tmp2 = Runtime.showFunctionContChain(cont, tmp, tmp1, 0); return runtime.safeCall(globalThis.console.log(tmp2)) } static debugHandler(cont) { let tmp, tmp1, tmp2; - tmp = globalThis.Object.freeze(new globalThis.Map()); - tmp1 = globalThis.Object.freeze(new globalThis.Set()); + tmp = globalThis.Object.freeze(new globalThis.Map(undefined)); + tmp1 = globalThis.Object.freeze(new globalThis.Set(undefined)); tmp2 = Runtime.showHandlerContChain(cont, tmp, tmp1, 0); return runtime.safeCall(globalThis.console.log(tmp2)) } @@ -956,8 +945,8 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { if (scrut1 === true) { runtime.safeCall(globalThis.console.log("")); } - vis = globalThis.Object.freeze(new globalThis.Set()); - hl = globalThis.Object.freeze(new globalThis.Map()); + vis = globalThis.Object.freeze(new globalThis.Set(undefined)); + hl = globalThis.Object.freeze(new globalThis.Map(undefined)); tmp = globalThis.Object.freeze([ contTrace.last ]); @@ -1211,7 +1200,7 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { result = Runtime.enterHandleBlock(Runtime.StackDelayHandler, f); scrut = Runtime.curEffect !== null; if (scrut === true) { - throw globalThis.Object.freeze(new globalThis.Error("Effect crossed through stack safe boundary")) + throw globalThis.Object.freeze(new globalThis.Error("Effect crossed through stack safe boundary", undefined)) } lbl: while (true) { let scrut1, saved, scrut2, tmp3; @@ -1224,7 +1213,7 @@ lambda4 = (undefined, function (Runtime2, EffectHandle1, value) { result = tmp3; scrut2 = Runtime.curEffect !== null; if (scrut2 === true) { - throw globalThis.Object.freeze(new globalThis.Error("Effect crossed through stack safe boundary")) + throw globalThis.Object.freeze(new globalThis.Error("Effect crossed through stack safe boundary", undefined)) } continue lbl; } @@ -1266,9 +1255,9 @@ export { lambda4 as _$_modulePrivate_$_lambda4 }; export { lambda5 as _$_modulePrivate_$_lambda5 }; export { lambda$ as _$_modulePrivate_$_lambda$ }; export { lambda$1 as _$_modulePrivate_$_lambda$1 }; -export { Capture$scope301 as _$_modulePrivate_$_Capture$scope30 }; +export { Capture$scope311 as _$_modulePrivate_$_Capture$scope31 }; export { lambda$2 as _$_modulePrivate_$_lambda$2 }; -export { Capture$scope321 as _$_modulePrivate_$_Capture$scope32 }; +export { Capture$scope331 as _$_modulePrivate_$_Capture$scope33 }; export { lambda$3 as _$_modulePrivate_$_lambda$3 }; export { lambda$4 as _$_modulePrivate_$_lambda$4 }; export { continuation$ as _$_modulePrivate_$_continuation$ }; diff --git a/hkmc2/shared/src/test/mlscript-compile/Runtime.mls b/hkmc2/shared/src/test/mlscript-compile/Runtime.mls index 5294741092..7cf283f200 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Runtime.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Runtime.mls @@ -29,31 +29,34 @@ fun unreachable = throw Error("unreachable") fun assertFail(file: Str, line: Str) = throw Error("Assertion failed (" + file + ":" + line + ")") -fun checkArgs(functionName: Str, expected: Int, isUB: Bool, got: Int) = - if got < expected || isUB && got > expected do +// As in JavaScript, functions ignore extra arguments, so only missing arguments are checked. +// This lets functions with fewer parameters be passed as callbacks to, e.g., `Array#map`. +// The `expected` count does not include optional parameters, nor the rest parameter. +fun checkArgs(functionName: Str, expected: Int, got: Int) = + if got < expected do let name = if functionName.length > 0 then " '" + functionName + "'" else "" - // throw globalThis.Error("Function" + name + " expected " - // + (if isUB then "" else "at least ") - // + expected - // + " argument(s) but got " + got) - throw Error of "Function" + name + " expected " - + (if isUB then "" else "at least " ) + throw Error of "Function" + name + " expected at least " + expected + " argument" + (if expected === 1 then "" else "s") + " but got " + got +// Checks the arguments of a function whose variadic parameter is followed by trailing parameters. +// Lowering gathers all the arguments after the `leading` ones into `args` (see `Lowering.lowerTrailingParams`). +fun checkVariadicArgs(functionName: Str, expected: Int, leading: Int, args: Array) = + checkArgs(functionName, expected, leading + args.length) + fun checkSelect(sel, nme, qual) = - if sel === undefined then + if sel === unsafe.undefined then throw Error("Access to required field '" + nme + "' yielded 'undefined'") else qual.(nme + "$__checkNotMethod") sel fun safeCall(x) = - if x is undefined then () else x + if x is unsafe.undefined then () else x fun checkCall(x) = - if x is undefined + if x is unsafe.undefined then throw Error("MLscript call unexpectedly returned `undefined`, the forbidden value.") else x diff --git a/hkmc2/shared/src/test/mlscript-compile/SymbolKeyLibrary.mls b/hkmc2/shared/src/test/mlscript-compile/SymbolKeyLibrary.mls new file mode 100644 index 0000000000..82016577e2 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript-compile/SymbolKeyLibrary.mls @@ -0,0 +1,43 @@ +#lang(0.3.x) + +open annotations { jsSymbol, fresh } + + +@fresh val fileKey = Symbol("file key") + +module SymbolKeyLibrary { + @fresh val readKey = Symbol("library read") + @fresh private val privateKey = Symbol("private key") + class Reader { + @jsSymbol(readKey) fun read() = 55 + } + module Nested { + @fresh val key = Symbol("nested library key") + class Reader { + @jsSymbol(key) fun read() = 68 + } + } + private module PrivateKeys { + @fresh val key = Symbol("private module key") + } + class PrivatePathReader { + @jsSymbol(PrivateKeys.key) fun read() = 72 + } + fun make(value) = + class Local { + @jsSymbol(SymbolKeyLibrary.readKey) fun read() = value + } + new Local + class FileReader { + @jsSymbol(fileKey) fun read() = 64 + @jsSymbol(privateKey) fun readPriv() = 65 + } + fun makeLocal(value) = + @fresh val key = Symbol("factory key") + class Local { + @jsSymbol(key) fun read() = value + } + new Local +} + + diff --git a/hkmc2/shared/src/test/mlscript-compile/Term.mls b/hkmc2/shared/src/test/mlscript-compile/Term.mls index 0b1225835a..92acbcb08c 100644 --- a/hkmc2/shared/src/test/mlscript-compile/Term.mls +++ b/hkmc2/shared/src/test/mlscript-compile/Term.mls @@ -13,7 +13,7 @@ open Predef module Term with... class Symbol(val name: Str) -type Literal = null | undefined | Str | Int | Num | Bool +type Literal = null | unsafe.undefined | Str | Int | Num | Bool class Pattern with constructor @@ -53,7 +53,7 @@ class Term with Lit(val lit: Literal) Builtin(val name: Str) Ref(val sym: Symbol) - CSRef(val sym: Symbol, val base: Str, val file: Str | undefined) + CSRef(val sym: Symbol, val base: Str, val file: Str | unsafe.undefined) App(val lhs: Term, val rhs: Term) Sel(val prefix: Term, val nme: Str) DynSel(val prefix: Term, val fld: Term, val arrayIdx: Bool) @@ -74,7 +74,7 @@ class Term with class Context(val names: Map[Str, Int], val bindings: Map[Symbol, Str], val dependencies: Set[Str], val printOnly: Bool) with fun get(sym: Symbol): Str = - if bindings.get(sym) is ~undefined as name then name + if bindings.get(sym) is ~unsafe.undefined as name then name else if printOnly then sym.name else throw Error(StrOps.concat("Invalid binding name ", sym.name)) fun nest: Context = Context(names, new Map(bindings), dependencies, printOnly) @@ -138,7 +138,7 @@ fun show(t: Term, ctx: Context): Str = if t is Ref(sym) then ctx.get(sym) CSRef(Symbol(name), baseFile, file) then - if file is undefined then ctx.depends(baseFile) + if file is unsafe.undefined then ctx.depends(baseFile) else ctx.depends(path.join of path.dirname(baseFile), file) name Lit(lit) then String(lit) diff --git a/hkmc2/shared/src/test/mlscript-compile/apps/CSV.mls b/hkmc2/shared/src/test/mlscript-compile/apps/CSV.mls index 30d6e68b0e..716ff4ad22 100644 --- a/hkmc2/shared/src/test/mlscript-compile/apps/CSV.mls +++ b/hkmc2/shared/src/test/mlscript-compile/apps/CSV.mls @@ -8,11 +8,11 @@ open Predef // The default delimiter is the comma, but this can be overriden here. -data class CSV(strDelimiter) with ... +data class CSV(strDelimiter?) with ... // Check to see if the delimiter is defined. If not, then default to comma. -if strDelimiter is undefined then set strDelimiter = "," +if strDelimiter is null then set strDelimiter = "," val objPattern = new mut RegExp of // Delimiters. @@ -47,7 +47,7 @@ fun toArrays(strData: Str) = // Now that we have our delimiter out of the way, // let's check to see which kind of value we captured (quoted or unquoted). let strMatchedValue = - if arrMatches.2 != undefined + if arrMatches.2 != unsafe.undefined then // We found a quoted value. When we capture this value, unescape any double quotes. arrMatches.2.replace of diff --git a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/ParseRule.mls b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/ParseRule.mls index b4096f4ca0..ae6200046d 100644 --- a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/ParseRule.mls +++ b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/ParseRule.mls @@ -60,25 +60,25 @@ module Choice with let shouldHaveFunction(options, key: Str, defaultValue, callerName: Str) = if let func = options.(key) typeof(func) === "function" then func - func is undefined then defaultValue + func is unsafe.undefined then defaultValue else throw TypeError(callerName + ": `" + key + "` is not a string") let shouldHaveStr(options, key: Str, defaultValue: Str, callerName: Str) = if let value = options.(key) value is Str then value - value is undefined then defaultValue + value is unsafe.undefined then defaultValue else throw TypeError(callerName + ": `" + key + "` is not a string") let shouldHaveInt(options, key: Str, callerName: Str) = if let value = options.(key) value is Int then Some(value) - value is undefined then None + value is unsafe.undefined then None else throw TypeError(callerName + ": `" + key + "` is not an Int") let shouldHaveBool(options, key: Str, defaultValue: Bool, callerName: Str) = if let value = options.(key) value is Bool then value - value is undefined then defaultValue + value is unsafe.undefined then defaultValue else throw TypeError(callerName + ": `" + key + "` is not a Boolean") let shouldHaveRuleLike(options, key: Str, ruleName: Str, callerName: Str) = if @@ -88,7 +88,7 @@ module Choice with choices is [..._] then ensureChoices(choices, "Choice.reference") rule(ruleName, ...choices) - choices is undefined then rule(ruleName) + choices is unsafe.undefined then rule(ruleName) else throw TypeError(callerName + ": `" + key + "` is neither a rule nor a choice") fun reference(kind: Str)(fields: Object) = 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 86ac2b4aa6..12b7b0a06b 100644 --- a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Rules.mls +++ b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Rules.mls @@ -36,8 +36,8 @@ fun idFirst(value, _) = value fun idSecond(_, value) = value fun someFirst(value, _) = Some(value) fun listFirst(value, _) = value :: Nil -fun listLike(fields: {head: Str, tail: Str, name: Str, sep: Keywords.Keyword | undefined}) = - let mkTail = if fields.("sep") is undefined then id else keyword(fields.("sep")) +fun listLike(fields: {head: Str, tail: Str, name: Str, sep: Keywords.Keyword | unsafe.undefined}) = + let mkTail = if fields.("sep") is unsafe.undefined then id else keyword(fields.("sep")) reference(fields.head) of process: Cons, name: "the first " + fields.name choices: tuple of end(Nil), mkTail of reference(fields.tail) of diff --git a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Test.mls b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Test.mls index ad1c063e05..515af2033a 100644 --- a/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Test.mls +++ b/hkmc2/shared/src/test/mlscript-compile/apps/parsing/Test.mls @@ -24,7 +24,7 @@ module Test with ... pattern Flag = "tree" | "trace" | "tokens" fun flags(tokens) = - let result = new Set + let result = new Set() while tokens is Token.Space :: Token.Identifier(":", _) :: Token.Identifier(Flag as flag, _) :: tail then result.add(flag) diff --git a/hkmc2/shared/src/test/mlscript-compile/wasm/Wasm.mls b/hkmc2/shared/src/test/mlscript-compile/wasm/Wasm.mls index 0f7d03c61c..84477a7714 100644 --- a/hkmc2/shared/src/test/mlscript-compile/wasm/Wasm.mls +++ b/hkmc2/shared/src/test/mlscript-compile/wasm/Wasm.mls @@ -49,7 +49,7 @@ fun binaryenCompileToModule(wat, importObject): dyn = fun unwrapWasmException(err: dyn, tag) = if ( err !== null && - err !== undefined && + err !== unsafe.undefined && typeof(err.is) === "function" && typeof(err.getArg) === "function" && err.is(tag) diff --git a/hkmc2/shared/src/test/mlscript/apps/CSVTest.mls b/hkmc2/shared/src/test/mlscript/apps/CSVTest.mls index 5b8b9f6fca..48587dcc0c 100644 --- a/hkmc2/shared/src/test/mlscript/apps/CSVTest.mls +++ b/hkmc2/shared/src/test/mlscript/apps/CSVTest.mls @@ -4,7 +4,7 @@ import "../../mlscript-compile/apps/CSV.mls" -let csv = CSV(undefined) +let csv = CSV() //│ csv = CSV(",") diff --git a/hkmc2/shared/src/test/mlscript/backlog/NewWith.mls b/hkmc2/shared/src/test/mlscript/backlog/NewWith.mls index e26adac58e..44347168d2 100644 --- a/hkmc2/shared/src/test/mlscript/backlog/NewWith.mls +++ b/hkmc2/shared/src/test/mlscript/backlog/NewWith.mls @@ -9,7 +9,7 @@ new //│ ╙── ^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. -new Object +new Object() //│ = {} :pe @@ -40,26 +40,26 @@ new Object with 1 //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. :e -(new Object) with 1 +(new Object()) with 1 //│ ╔══[COMPILATION ERROR] Unexpected infix use of keyword 'with' here -//│ ║ l.43: (new Object) with 1 -//│ ╙── ^^^^^^^^^^^^^^^^^^^ +//│ ║ l.43: (new Object()) with 1 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. // TODO don't show as `$anon` -new Object with { val x = 1 } +new Object() with { val x = 1 } //│ = $anon { x: 1 } :e -new Object with { val x = 1 } with { val y = 1 } +new Object() with { val x = 1 } with { val y = 1 } //│ ╔══[COMPILATION ERROR] Unexpected infix use of keyword 'with' here -//│ ║ l.54: new Object with { val x = 1 } with { val y = 1 } -//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.54: new Object() with { val x = 1 } with { val y = 1 } +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. let obj = - new Object with + new Object() with val x = 12 //│ obj = $anon { x: 12 } diff --git a/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls b/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls index cd2c42c7b2..99430e0d95 100644 --- a/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls +++ b/hkmc2/shared/src/test/mlscript/backlog/ToTriage.mls @@ -132,27 +132,16 @@ c.x // ——— ——— ——— -:breakme // TODO: warn against using `undefined` -:w -fun oops = undefined - -// * This used to throw, but we now emit `Unit` upon compiling MIR's `return undefined` -:noInline -print(oops) -//│ > () - -// ——— ——— ——— - class C :sjs class D extends id(C) //│ ╔══[COMPILATION ERROR] Expected a statically known class; found selection. -//│ ║ l.149: class D extends id(C) +//│ ║ l.138: class D extends id(C) //│ ║ ^^ //│ ╙── The 'new' keyword requires a statically known class; use the 'new!' operator for dynamic instantiation. //│ ╔══[COMPILATION ERROR] Too many parameter lists for parent class -//│ ║ l.149: class D extends id(C) +//│ ║ l.138: class D extends id(C) //│ ╙── ^^^^^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let D1, superBase; @@ -181,14 +170,14 @@ let x = 0 :fixme set x += 1; () //│ ╔══[COMPILATION ERROR] Expected a right-hand side for this assignment -//│ ║ l.182: set x += 1; () +//│ ║ l.171: set x += 1; () //│ ╙── ^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. :e set (x += 1; ()) //│ ╔══[COMPILATION ERROR] Expected a right-hand side for this assignment -//│ ║ l.189: set (x += 1; ()) +//│ ║ l.178: set (x += 1; ()) //│ ╙── ^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -197,7 +186,7 @@ set (x += 1; ()) :fixme [x, set x += 1; x] //│ ╔══[COMPILATION ERROR] Expected a right-hand side for this assignment -//│ ║ l.198: [x, set x += 1; x] +//│ ║ l.187: [x, set x += 1; x] //│ ╙── ^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -208,13 +197,13 @@ import "../../mlscript-compile/Stack.mls" // The parser rejects this, but it should be allowed. open Stack { ::, Nil } //│ ╔══[COMPILATION ERROR] Illegal 'open' statement shape. -//│ ║ l.209: open Stack { ::, Nil } +//│ ║ l.198: open Stack { ::, Nil } //│ ╙── ^^^^^^^^^^^^^^^ // Instead, if we parenthesize the operator, it is rejected by the elaborator. open Stack { (::), Nil } //│ ╔══[COMPILATION ERROR] Illegal 'open' statement element. -//│ ║ l.215: open Stack { (::), Nil } +//│ ║ l.204: open Stack { (::), Nil } //│ ╙── ^^^^ open Stack { Nil, :: } @@ -240,46 +229,46 @@ id(Foo)(1) :e data class FooSpd(...args) //│ ╔══[COMPILATION ERROR] Spread parameters are not supported in class parameters. -//│ ║ l.241: data class FooSpd(...args) +//│ ║ l.230: data class FooSpd(...args) //│ ╙── ^^^^^^^ FooSpd(1, 2, 3).args //│ ╔══[COMPILATION ERROR] Expected 0 arguments, got 3 -//│ ║ l.246: FooSpd(1, 2, 3).args +//│ ║ l.235: FooSpd(1, 2, 3).args //│ ╙── ^^^^^^^^^ //│ ═══[RUNTIME ERROR] Error: Access to required field 'args' yielded 'undefined' :todo if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╔══[COMPILATION ERROR] Unrecognized pattern (spread). -//│ ║ l.253: if FooSpd(1, 2, 3) is FooSpd(...args) then args +//│ ║ l.242: if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╙── ^^^^^^^ //│ ╔══[COMPILATION ERROR] Name not found: args -//│ ║ l.253: if FooSpd(1, 2, 3) is FooSpd(...args) then args +//│ ║ l.242: if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╙── ^^^^ //│ ╔══[COMPILATION ERROR] Expected 0 arguments, got 3 -//│ ║ l.253: if FooSpd(1, 2, 3) is FooSpd(...args) then args +//│ ║ l.242: if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╙── ^^^^^^^^^ //│ ╔══[COMPILATION ERROR] The constructor does not take any arguments but found one argument. -//│ ║ l.253: if FooSpd(1, 2, 3) is FooSpd(...args) then args +//│ ║ l.242: if FooSpd(1, 2, 3) is FooSpd(...args) then args //│ ╙── ^^^^^^ //│ ═══[RUNTIME ERROR] Error: match error if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] //│ ╔══[COMPILATION ERROR] Expected 0 arguments, got 3 -//│ ║ l.268: if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] +//│ ║ l.257: if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] //│ ╙── ^^^^^^^^^ //│ ╔══[COMPILATION ERROR] The constructor does not take any arguments but found three arguments. -//│ ║ l.268: if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] +//│ ║ l.257: if FooSpd(1, 2, 3) is FooSpd(a, b, c) then [a, b, c] //│ ╙── ^^^^^^^ //│ ═══[RUNTIME ERROR] Error: match error if FooSpd(1, 2, 3) is FooSpd(arg) then arg //│ ╔══[COMPILATION ERROR] Expected 0 arguments, got 3 -//│ ║ l.277: if FooSpd(1, 2, 3) is FooSpd(arg) then arg +//│ ║ l.266: if FooSpd(1, 2, 3) is FooSpd(arg) then arg //│ ╙── ^^^^^^^^^ //│ ╔══[COMPILATION ERROR] The constructor does not take any arguments but found one argument. -//│ ║ l.277: if FooSpd(1, 2, 3) is FooSpd(arg) then arg +//│ ║ l.266: if FooSpd(1, 2, 3) is FooSpd(arg) then arg //│ ╙── ^^^ //│ ═══[RUNTIME ERROR] Error: match error @@ -290,10 +279,10 @@ fun foo() = let bar(x: Str): Str = x + x bar("test") //│ ╔══[COMPILATION ERROR] Unsupported let binding shape -//│ ║ l.290: let bar(x: Str): Str = x + x +//│ ║ l.279: let bar(x: Str): Str = x + x //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Name not found: bar -//│ ║ l.291: bar("test") +//│ ║ l.280: bar("test") //│ ╙── ^^^ // ——— ——— ——— @@ -307,7 +296,7 @@ module Foo(x) with print(x) //│ ╔══[COMPILATION ERROR] No definition found in scope for member 'x' //│ ╟── which references the symbol introduced here -//│ ║ l.306: module Foo(x) with +//│ ║ l.295: module Foo(x) with //│ ╙── ^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let Foo5; @@ -325,7 +314,7 @@ module Foo(x) with //│ }); //│ ╔══[COMPILATION ERROR] No definition found in scope for member 'x' //│ ╟── which references the symbol introduced here -//│ ║ l.306: module Foo(x) with +//│ ║ l.295: module Foo(x) with //│ ╙── ^ //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ > 1 @@ -335,7 +324,7 @@ module Foo(x) with module Foo(val x) //│ ╔══[COMPILATION ERROR] No definition found in scope for member 'x' //│ ╟── which references the symbol introduced here -//│ ║ l.335: module Foo(val x) +//│ ║ l.324: module Foo(val x) //│ ╙── ^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let Foo7; @@ -351,7 +340,7 @@ module Foo(val x) //│ }); //│ ╔══[COMPILATION ERROR] No definition found in scope for member 'x' //│ ╟── which references the symbol introduced here -//│ ║ l.335: module Foo(val x) +//│ ║ l.324: module Foo(val x) //│ ╙── ^ //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -360,7 +349,7 @@ module Foo(val x) // TODO support syntax? data class Foo(mut x) //│ ╔══[COMPILATION ERROR] Expected a valid parameter, found 'mut'-modified identifier -//│ ║ l.361: data class Foo(mut x) +//│ ║ l.350: data class Foo(mut x) //│ ╙── ^ // ——— ——— ——— @@ -369,16 +358,16 @@ data class Foo(mut x) :e class Id(name: UnboundName) //│ ╔══[COMPILATION ERROR] Name not found: UnboundName -//│ ║ l.370: class Id(name: UnboundName) +//│ ║ l.359: class Id(name: UnboundName) //│ ╙── ^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Expected a type, got a non-type ‹error› -//│ ║ l.370: class Id(name: UnboundName) +//│ ║ l.359: class Id(name: UnboundName) //│ ╙── ^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Expected a type, got ‹error› -//│ ║ l.370: class Id(name: UnboundName) +//│ ║ l.359: class Id(name: UnboundName) //│ ╙── ^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Expected a type, got a non-type ‹error› -//│ ║ l.370: class Id(name: UnboundName) +//│ ║ l.359: class Id(name: UnboundName) //│ ╙── ^^^^^^^^^^^ // ——— ——— ——— diff --git a/hkmc2/shared/src/test/mlscript/basics/BadMemberProjections.mls b/hkmc2/shared/src/test/mlscript/basics/BadMemberProjections.mls index 48da72d385..c1d6958910 100644 --- a/hkmc2/shared/src/test/mlscript/basics/BadMemberProjections.mls +++ b/hkmc2/shared/src/test/mlscript/basics/BadMemberProjections.mls @@ -33,13 +33,13 @@ //│ ╔══[COMPILATION ERROR] Resolution error in application //│ ║ l.26: 1::x() //│ ║ ^^^^^ -//│ ╟── Function literal expected 1 argument, but got 0 +//│ ╟── Function literal expected at least 1 argument, but got 0 //│ ║ l.26: 1::x() //│ ╙── ^ //│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— //│ let lambda1; //│ lambda1 = (undefined, function (self, ...args) { -//│ runtime.checkArgs("", 1, false, arguments.length); +//│ runtime.checkArgs("", 1, arguments.length); //│ throw "This code cannot be run as its compilation yielded an error." //│ }); //│ block$res2 = runtime.safeCall(lambda1()); diff --git a/hkmc2/shared/src/test/mlscript/basics/BadModuleUses.mls b/hkmc2/shared/src/test/mlscript/basics/BadModuleUses.mls index 4c25e64e78..4b5f8f0e85 100644 --- a/hkmc2/shared/src/test/mlscript/basics/BadModuleUses.mls +++ b/hkmc2/shared/src/test/mlscript/basics/BadModuleUses.mls @@ -47,14 +47,14 @@ Example.foo() + 1 //│ ╔══[COMPILATION ERROR] Unexpected moduleful application of type M. //│ ║ l.46: Example.foo() + 1 //│ ╙── ^^^^^^^^^^^^^ -//│ = "class M { static { M1 = this } static mtd() { runtime.checkArgs(\"mtd\", 0, true, arguments.length); return 42 } toString() { return runtime.render(this); } static [definitionMetadata] = [\"class\", \"M\"]; }1" +//│ = "class M { static { M1 = this } static mtd() { runtime.checkArgs(\"mtd\", 0, arguments.length); return 42 } toString() { return runtime.render(this); } static [definitionMetadata] = [\"class\", \"M\"]; }1" :e M + 1 //│ ╔══[COMPILATION ERROR] Unexpected moduleful reference of type M. //│ ║ l.53: M + 1 //│ ╙── ^ -//│ = "class M { static { M1 = this } static mtd() { runtime.checkArgs(\"mtd\", 0, true, arguments.length); return 42 } toString() { return runtime.render(this); } static [definitionMetadata] = [\"class\", \"M\"]; }1" +//│ = "class M { static { M1 = this } static mtd() { runtime.checkArgs(\"mtd\", 0, arguments.length); return 42 } toString() { return runtime.render(this); } static [definitionMetadata] = [\"class\", \"M\"]; }1" :e M |> id diff --git a/hkmc2/shared/src/test/mlscript/basics/BadParams.mls b/hkmc2/shared/src/test/mlscript/basics/BadParams.mls index c92a5fbcf1..31015b997e 100644 --- a/hkmc2/shared/src/test/mlscript/basics/BadParams.mls +++ b/hkmc2/shared/src/test/mlscript/basics/BadParams.mls @@ -52,16 +52,16 @@ fun f(val x) = x :e (...x, ...y) => x -//│ ╔══[COMPILATION ERROR] Spread parameters must be the last in the parameter list. +//│ ╔══[COMPILATION ERROR] A parameter list cannot have more than one variadic parameter. //│ ║ l.54: (...x, ...y) => x -//│ ╙── ^^^^ +//│ ╙── ^^^^ //│ = fun :e (..., ...y) => y -//│ ╔══[COMPILATION ERROR] Spread parameters must be the last in the parameter list. +//│ ╔══[COMPILATION ERROR] A parameter list cannot have more than one variadic parameter. //│ ║ l.61: (..., ...y) => y -//│ ╙── ^^^ +//│ ╙── ^^^^ //│ = fun diff --git a/hkmc2/shared/src/test/mlscript/basics/CompanionFunctions.mls b/hkmc2/shared/src/test/mlscript/basics/CompanionFunctions.mls index 14aff0a869..751e82bb0c 100644 --- a/hkmc2/shared/src/test/mlscript/basics/CompanionFunctions.mls +++ b/hkmc2/shared/src/test/mlscript/basics/CompanionFunctions.mls @@ -28,7 +28,7 @@ Foo(123, 456) //│ ╟── Function 'Foo' expected 1 argument, but got 2 //│ ║ l.17: fun Foo(x) = new Foo(x, 0) //│ ╙── ^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'Foo' expected 1 argument but got 2 +//│ = Foo :e new Foo(123) diff --git a/hkmc2/shared/src/test/mlscript/basics/Declare.mls b/hkmc2/shared/src/test/mlscript/basics/Declare.mls index 575b9faed5..07f5f727c7 100644 --- a/hkmc2/shared/src/test/mlscript/basics/Declare.mls +++ b/hkmc2/shared/src/test/mlscript/basics/Declare.mls @@ -91,3 +91,10 @@ M.D.oops //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. +// A symbolic source alias must still select the declared global property. +:expect [42, 42] +declare fun (%%) parseInt(text: Str, radix: Int): Int +[parseInt("42", 10), "2a" %% 16] +//│ = [42, 42] + + diff --git a/hkmc2/shared/src/test/mlscript/basics/DynamicFields.mls b/hkmc2/shared/src/test/mlscript/basics/DynamicFields.mls index 763328b058..d8166ed048 100644 --- a/hkmc2/shared/src/test/mlscript/basics/DynamicFields.mls +++ b/hkmc2/shared/src/test/mlscript/basics/DynamicFields.mls @@ -73,3 +73,11 @@ new DynCtor().dynField //│ = 42 +// * A literal receiver is parenthesized: `-1["toFixed"]` would parse as `-(1["toFixed"])`. +:sjs +(-1).("toFixed")(1) +//│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— +//│ return runtime.safeCall((-1)["toFixed"](1)) +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = "-1.0" + diff --git a/hkmc2/shared/src/test/mlscript/basics/LazySpreads.mls b/hkmc2/shared/src/test/mlscript/basics/LazySpreads.mls index 1d6015100f..fb4c631f3e 100644 --- a/hkmc2/shared/src/test/mlscript/basics/LazySpreads.mls +++ b/hkmc2/shared/src/test/mlscript/basics/LazySpreads.mls @@ -117,7 +117,7 @@ sum2(..arr) //│ ╔══[COMPILATION ERROR] Lazy spreads are not supported in call arguments //│ ║ l.86: sum2(..arr) //│ ╙── ^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function expected 1 argument but got 2 +//│ ═══[RUNTIME ERROR] Error: match error fun map2(f) = case diff --git a/hkmc2/shared/src/test/mlscript/basics/Literals.mls b/hkmc2/shared/src/test/mlscript/basics/Literals.mls index 6574f18ba1..0cb47465e7 100644 --- a/hkmc2/shared/src/test/mlscript/basics/Literals.mls +++ b/hkmc2/shared/src/test/mlscript/basics/Literals.mls @@ -46,7 +46,13 @@ null //│ = null +:w undefined +//│ ╔══[WARNING] Direct use of 'undefined' is discouraged; use 'unsafe.undefined' for JavaScript interoperation. +//│ ║ l.50: undefined +//│ ╙── ^^^^^^^^^ + +unsafe.undefined 0.5 @@ -55,7 +61,7 @@ undefined :todo NaN //│ ╔══[COMPILATION ERROR] Name not found: NaN -//│ ║ l.56: NaN +//│ ║ l.62: NaN //│ ╙── ^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -75,7 +81,7 @@ NaN :e 'AA BB' //│ ╔══[COMPILATION ERROR] Illegal juxtaposition right-hand side (identifier). -//│ ║ l.76: 'AA BB' +//│ ║ l.82: 'AA BB' //│ ╙── ^^^ //│ = "AA" @@ -98,42 +104,42 @@ NaN :w "\uFE&&" //│ ╔══[WARNING] Invalid Unicode escape sequence: expected 4 hexadecimal digits but got 2 -//│ ║ l.99: "\uFE&&" -//│ ╙── ^^ +//│ ║ l.105: "\uFE&&" +//│ ╙── ^^ //│ = "&&" :w "\u{}" //│ ╔══[WARNING] Expected at least one hexadecimal digit in Unicode escape sequence -//│ ║ l.106: "\u{}" +//│ ║ l.112: "\u{}" //│ ╙── ^ //│ = "" :w "\u{ffffffff}" //│ ╔══[WARNING] Too many hexadecimal digits in Unicode escape sequence -//│ ║ l.113: "\u{ffffffff}" +//│ ║ l.119: "\u{ffffffff}" //│ ╙── ^^ //│ = "" :w "\u{efefef}" //│ ╔══[WARNING] Unicode code point out of range: 0xefefef -//│ ║ l.120: "\u{efefef}" +//│ ║ l.126: "\u{efefef}" //│ ╙── ^^^^^^ //│ = "" :w "\u{55" //│ ╔══[WARNING] Unterminated Unicode escape sequence: missing '}' -//│ ║ l.127: "\u{55" +//│ ║ l.133: "\u{55" //│ ╙── ^ //│ = "U" :w "\u{41****" //│ ╔══[WARNING] Unterminated Unicode escape sequence: missing '}' -//│ ║ l.134: "\u{41****" +//│ ║ l.140: "\u{41****" //│ ╙── ^ //│ = "A****" diff --git a/hkmc2/shared/src/test/mlscript/basics/MemberProjections.mls b/hkmc2/shared/src/test/mlscript/basics/MemberProjections.mls index 2af6a31173..9457d10ae3 100644 --- a/hkmc2/shared/src/test/mlscript/basics/MemberProjections.mls +++ b/hkmc2/shared/src/test/mlscript/basics/MemberProjections.mls @@ -92,7 +92,7 @@ foo.M.Foo::m() //│ ╔══[COMPILATION ERROR] Resolution error in application //│ ║ l.91: foo.M.Foo::m() //│ ║ ^^^^^ -//│ ╟── Function literal expected 1 argument, but got 0 +//│ ╟── Function literal expected at least 1 argument, but got 0 //│ ║ l.91: foo.M.Foo::m() //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Resolution error in selection diff --git a/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls b/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls index b4ba00a2c6..b038093965 100644 --- a/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls +++ b/hkmc2/shared/src/test/mlscript/basics/MiscArrayTests.mls @@ -27,17 +27,8 @@ f() xs.reverse() //│ = [2, 1, 0] -:e -:re xs.map(x => x * 2) -//│ ╔══[COMPILATION ERROR] Resolution error in function type -//│ ║ l.261: fun map[U](f: (T, Int, Array[T]) -> U): Array[U] -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ -//│ ╟── Callback parameter list does not match its declared function type. -//│ ║ in MiscArrayTests.mls:32 -//│ ║ l.32: xs.map(x => x * 2) -//│ ╙── ^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function expected 1 argument but got 3 +//│ = [4, 2, 0] xs.map((x, i, a) => x * 2) //│ = [4, 2, 0] @@ -60,10 +51,10 @@ xs |.map(x => x * 2) |.print() xs |. map(x => x * 2) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.61: map(x => x * 2) +//│ ║ l.52: map(x => x * 2) //│ ║ ^^^^^^^^^^^^^^^ //│ ╟── Function 'map' expected 2 arguments, but got 1 -//│ ║ l.51: fun map(xs: Array, f) = xs.map((x, ...bs) => f(x)) +//│ ║ l.42: fun map(xs: Array, f) = xs.map((x, ...bs) => f(x)) //│ ╙── ^^^^^^^^^^^^^^^^^^^^^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let lambda5, tmp; @@ -73,19 +64,19 @@ xs |. //│ tmp = map(lambda5); //│ return Predef.passTo(xs1, tmp) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ ═══[RUNTIME ERROR] Error: Function 'map' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'map' expected at least 2 arguments but got 1 :e :re xs|. map(x => x * 2) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.81: map(x => x * 2) +//│ ║ l.72: map(x => x * 2) //│ ║ ^^^^^^^^^^^^^^^ //│ ╟── Function 'map' expected 2 arguments, but got 1 -//│ ║ l.51: fun map(xs: Array, f) = xs.map((x, ...bs) => f(x)) +//│ ║ l.42: fun map(xs: Array, f) = xs.map((x, ...bs) => f(x)) //│ ╙── ^^^^^^^^^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'map' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'map' expected at least 2 arguments but got 1 :pt xs diff --git a/hkmc2/shared/src/test/mlscript/basics/New.mls b/hkmc2/shared/src/test/mlscript/basics/New.mls index e97677f848..daf288f733 100644 --- a/hkmc2/shared/src/test/mlscript/basics/New.mls +++ b/hkmc2/shared/src/test/mlscript/basics/New.mls @@ -35,12 +35,6 @@ new :re new Foo() -//│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.37: Foo() -//│ ║ ^^^^^ -//│ ╟── Class 'Foo' must be instantiated with 'new'. -//│ ║ l.5: class Foo -//│ ╙── ^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Resolution error in object instantiation //│ ║ l.36: new //│ ║ ^^^ @@ -49,6 +43,12 @@ new //│ ╟── Invalid class expression: application //│ ║ l.37: Foo() //│ ╙── ^^^^^ +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.37: Foo() +//│ ║ ^^^^^ +//│ ╟── Class 'Foo' must be instantiated with 'new'. +//│ ║ l.5: class Foo +//│ ╙── ^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. :e diff --git a/hkmc2/shared/src/test/mlscript/basics/NewMut.mls b/hkmc2/shared/src/test/mlscript/basics/NewMut.mls index 4015d33f1f..5fadb7f366 100644 --- a/hkmc2/shared/src/test/mlscript/basics/NewMut.mls +++ b/hkmc2/shared/src/test/mlscript/basics/NewMut.mls @@ -168,7 +168,7 @@ f.y -let f = (new Object with +let f = (new Object() with val x = 12) as dyn //│ f = $anon { x: 12 } @@ -184,7 +184,7 @@ f.y //│ ═══[RUNTIME ERROR] Error: Access to required field 'y' yielded 'undefined' -let f = (new mut Object with +let f = (new mut Object() with val x = 12) as dyn //│ f = $anon { x: 12 } diff --git a/hkmc2/shared/src/test/mlscript/basics/OfLambdaArgs.mls b/hkmc2/shared/src/test/mlscript/basics/OfLambdaArgs.mls index b244fc91b4..ee3ec7de9a 100644 --- a/hkmc2/shared/src/test/mlscript/basics/OfLambdaArgs.mls +++ b/hkmc2/shared/src/test/mlscript/basics/OfLambdaArgs.mls @@ -27,6 +27,6 @@ foo(id of x => 1, id of y => 2) //│ ╟── Function 'foo' expected 2 arguments, but got 1 //│ ║ l.6: fun foo(f, g) = f(g(1)) //│ ╙── ^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'id' expected 1 argument but got 2 +//│ ═══[RUNTIME ERROR] Error: Function 'foo' expected at least 2 arguments but got 1 diff --git a/hkmc2/shared/src/test/mlscript/basics/Records.mls b/hkmc2/shared/src/test/mlscript/basics/Records.mls index 8e00e138a2..a65ebffb3e 100644 --- a/hkmc2/shared/src/test/mlscript/basics/Records.mls +++ b/hkmc2/shared/src/test/mlscript/basics/Records.mls @@ -212,7 +212,7 @@ t('group) //│ ╟── Function 't' expected 2 arguments, but got 1 //│ ║ l.176: fun t(kind, props) = //│ ╙── ^^^^^^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 't' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 't' expected at least 2 arguments but got 1 :todo // * Make second parameter optional t('group) @@ -222,7 +222,7 @@ t('group) //│ ╟── Function 't' expected 2 arguments, but got 1 //│ ║ l.176: fun t(kind, props) = //│ ╙── ^^^^^^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 't' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 't' expected at least 2 arguments but got 1 let empty = Object.create of null //│ empty = {} diff --git a/hkmc2/shared/src/test/mlscript/block-staging/PrintCode.mls b/hkmc2/shared/src/test/mlscript/block-staging/PrintCode.mls index 0029728606..780ad22c8e 100644 --- a/hkmc2/shared/src/test/mlscript/block-staging/PrintCode.mls +++ b/hkmc2/shared/src/test/mlscript/block-staging/PrintCode.mls @@ -14,7 +14,7 @@ printCode(FunDefn(Symbol("f"), [[Symbol("x")]], Return(ValueLit(1)))) printCode(ValueLit(true)) printCode(ValueLit(null)) -printCode(ValueLit(undefined)) +printCode(ValueLit(unsafe.undefined)) printCode(ValueLit("c")) printCode(ValueLit("string")) //│ > true diff --git a/hkmc2/shared/src/test/mlscript/codegen/AuxiliaryConstructors.mls b/hkmc2/shared/src/test/mlscript/codegen/AuxiliaryConstructors.mls index 0b3f4e146b..9c0bb10c7d 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/AuxiliaryConstructors.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/AuxiliaryConstructors.mls @@ -159,6 +159,12 @@ class C :e class D extends C(0) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.161: class D extends C(0) +//│ ║ ^^^^ +//│ ╟── Class value expected 0 arguments, but got 1 +//│ ║ l.158: class C +//│ ╙── ^^^^^^^ //│ ╔══[COMPILATION ERROR] Too many parameter lists for parent class //│ ║ l.161: class D extends C(0) //│ ╙── ^^^^ @@ -167,9 +173,9 @@ class D extends C(0) new A(1)(2) with fun a = 0 //│ ╔══[COMPILATION ERROR] Extending a partially applied class is not supported -//│ ║ l.167: new A(1)(2) with +//│ ║ l.173: new A(1)(2) with //│ ║ ^^^^^^^^^^^^^^^^ -//│ ║ l.168: fun a = 0 +//│ ║ l.174: fun a = 0 //│ ╙── ^^^^^^^^^^^ //│ = $anon { x: 1 } @@ -177,9 +183,20 @@ new A(1)(2) with new A(1)(2)(3)(4) with fun a = 0 //? = $anon { x: 1 } +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.183: new A(1)(2)(3)(4) with +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.184: fun a = 0 +//│ ║ ^^^^^^^^^^^ +//│ ╟── Class 'A' expected at most 3 argument lists, but got 4 +//│ ║ l.139: class A(val x) with +//│ ║ ^^^^^^^^^^^^^^ +//│ ╟── The constructor is declared here. +//│ ║ l.140: constructor(y)(z) +//│ ╙── ^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Too many parameter lists for parent class -//│ ║ l.177: new A(1)(2)(3)(4) with +//│ ║ l.183: new A(1)(2)(3)(4) with //│ ║ ^^^^^^^^^^^^^^^^^^^^^^ -//│ ║ l.178: fun a = 0 +//│ ║ l.184: fun a = 0 //│ ╙── ^^^^^^^^^^^ //│ = $anon { x: 1 } diff --git a/hkmc2/shared/src/test/mlscript/codegen/BadSpreads.mls b/hkmc2/shared/src/test/mlscript/codegen/BadSpreads.mls index d476ea0864..04f3e01350 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/BadSpreads.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/BadSpreads.mls @@ -22,21 +22,16 @@ fun f = ..., 1 //│ ═══[RUNTIME ERROR] TypeError: 1 is not iterable -:e +:expect [1, 2, 3] fun foo(a, ...b, c) = [a, ...b, c] -//│ ╔══[COMPILATION ERROR] Spread parameters must be the last in the parameter list. -//│ ║ l.26: fun foo(a, ...b, c) = [a, ...b, c] -//│ ╙── ^^^^ +foo(1, 2, 3) +//│ = [1, 2, 3] -:e fun foo(a, ...b, c) = [a, ...b] -//│ ╔══[COMPILATION ERROR] Spread parameters must be the last in the parameter list. -//│ ║ l.32: fun foo(a, ...b, c) = [a, ...b] -//│ ╙── ^^^^ -:re +:expect [1, 2] foo(1, 2, 3) -//│ ═══[RUNTIME ERROR] TypeError: 2 is not iterable +//│ = [1, 2] fun foo(a, ...b) = [a, ...b] @@ -45,10 +40,10 @@ fun foo(a, ...b) = [a, ...b] :re foo() //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.46: foo() +//│ ║ l.41: foo() //│ ║ ^^^^^ -//│ ╟── Function 'foo' expected 1 argument, but got 0 -//│ ║ l.42: fun foo(a, ...b) = [a, ...b] +//│ ╟── Function 'foo' expected at least 1 argument, but got 0 +//│ ║ l.37: fun foo(a, ...b) = [a, ...b] //│ ╙── ^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] Error: Function 'foo' expected at least 1 argument but got 0 @@ -56,13 +51,13 @@ foo() :e fun foo(1) = 2 //│ ╔══[COMPILATION ERROR] Expected a valid parameter, found integer literal -//│ ║ l.57: fun foo(1) = 2 +//│ ║ l.52: fun foo(1) = 2 //│ ╙── ^ :e fun foo(...1) = 2 //│ ╔══[COMPILATION ERROR] Expected a valid parameter, found spread -//│ ║ l.63: fun foo(...1) = 2 +//│ ║ l.58: fun foo(...1) = 2 //│ ╙── ^^^^ diff --git a/hkmc2/shared/src/test/mlscript/codegen/BuiltinOps.mls b/hkmc2/shared/src/test/mlscript/codegen/BuiltinOps.mls index b4fbd37596..60bdd341e0 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/BuiltinOps.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/BuiltinOps.mls @@ -84,7 +84,7 @@ id(+)(1) //│ }); //│ return runtime.safeCall(lambda3(1)) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ ═══[RUNTIME ERROR] Error: Function expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function expected at least 2 arguments but got 1 fun (+) lol(a, b) = [a, b] diff --git a/hkmc2/shared/src/test/mlscript/codegen/BuiltinSymbols.mls b/hkmc2/shared/src/test/mlscript/codegen/BuiltinSymbols.mls index 75c928c20d..38265d1689 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/BuiltinSymbols.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/BuiltinSymbols.mls @@ -44,7 +44,7 @@ case Int then 0 //│ sym = member:lambda⁰ //│ dSym = term:lambda¹ //│ params = Ls of -//│ ParamList: +//│ LoweredParamList: //│ params = Ls of //│ Param: //│ sym = caseScrut⁰ diff --git a/hkmc2/shared/src/test/mlscript/codegen/ClassInFun.mls b/hkmc2/shared/src/test/mlscript/codegen/ClassInFun.mls index c1c15042dd..f2b62ab725 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ClassInFun.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ClassInFun.mls @@ -76,6 +76,6 @@ test() //│ ╟── Function 'test' expected 1 argument, but got 0 //│ ║ l.38: fun test(x) = //│ ╙── ^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'test' expected 1 argument but got 0 +//│ ═══[RUNTIME ERROR] Error: Function 'test' expected at least 1 argument but got 0 diff --git a/hkmc2/shared/src/test/mlscript/codegen/CurriedClassInheritance.mls b/hkmc2/shared/src/test/mlscript/codegen/CurriedClassInheritance.mls index 0e0abb2164..33bbc60f20 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/CurriedClassInheritance.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/CurriedClassInheritance.mls @@ -49,33 +49,51 @@ let c = b(100) //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ c = undefined -:fixme +:ge +:re c.a -//│ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'a') +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.54: c.a +//│ ║ ^^^ +//│ ╟── Cannot resolve member 'a' of a value with unknown shape. +//│ ╟── The shape of this value is unknown. +//│ ║ l.42: let c = b(100) +//│ ╙── ^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. -:fixme +:ge +:re Bar(123)(1234)(100).a //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.57: Bar(123)(1234)(100).a +//│ ║ l.66: Bar(123)(1234)(100).a //│ ║ ^^^^^^^^^^^^^^^^^^^ //│ ╟── Instance of class 'Bar' cannot receive more argument lists. -//│ ║ l.57: Bar(123)(1234)(100).a +//│ ║ l.66: Bar(123)(1234)(100).a //│ ╙── ^^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.66: Bar(123)(1234)(100).a +//│ ║ ^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Cannot resolve member 'a' of a value with unknown shape. +//│ ╟── The shape of this value is unknown. +//│ ║ l.66: Bar(123)(1234)(100).a +//│ ╙── ^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. :ge :re [c.x, c.y] //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.68: [c.x, c.y] +//│ ║ l.84: [c.x, c.y] //│ ║ ^^^ -//│ ╟── Instance of class 'Bar' does not contain member 'x' +//│ ╟── Cannot resolve member 'x' of a value with unknown shape. +//│ ╟── The shape of this value is unknown. //│ ║ l.42: let c = b(100) //│ ╙── ^^^^^^ //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.68: [c.x, c.y] +//│ ║ l.84: [c.x, c.y] //│ ║ ^^^ -//│ ╟── Instance of class 'Bar' does not contain member 'y' +//│ ╟── Cannot resolve member 'y' of a value with unknown shape. +//│ ╟── The shape of this value is unknown. //│ ║ l.42: let c = b(100) //│ ╙── ^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -90,11 +108,11 @@ let b1 = b0(123) :re b1.a //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.91: b1.a -//│ ║ ^^^^ +//│ ║ l.109: b1.a +//│ ║ ^^^^ //│ ╟── Partially-applied constructor of class 'Bar' does not contain member 'a' -//│ ║ l.86: let b1 = b0(123) -//│ ╙── ^^^^^^^ +//│ ║ l.104: let b1 = b0(123) +//│ ╙── ^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. let b = b1(1234) @@ -107,32 +125,42 @@ b.a :re let c = b(100) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.108: let c = b(100) +//│ ║ l.126: let c = b(100) //│ ║ ^^^^^^ //│ ╟── Instance of class 'Bar' cannot receive more argument lists. -//│ ║ l.100: let b = b1(1234) +//│ ║ l.118: let b = b1(1234) //│ ╙── ^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ c = undefined -:fixme +:ge +:re c.a -//│ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'a') +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.138: c.a +//│ ║ ^^^ +//│ ╟── Cannot resolve member 'a' of a value with unknown shape. +//│ ╟── The shape of this value is unknown. +//│ ║ l.126: let c = b(100) +//│ ╙── ^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. :ge :re [c.x, c.y] //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.124: [c.x, c.y] +//│ ║ l.150: [c.x, c.y] //│ ║ ^^^ -//│ ╟── Instance of class 'Bar' does not contain member 'x' -//│ ║ l.108: let c = b(100) +//│ ╟── Cannot resolve member 'x' of a value with unknown shape. +//│ ╟── The shape of this value is unknown. +//│ ║ l.126: let c = b(100) //│ ╙── ^^^^^^ //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.124: [c.x, c.y] +//│ ║ l.150: [c.x, c.y] //│ ║ ^^^ -//│ ╟── Instance of class 'Bar' does not contain member 'y' -//│ ║ l.108: let c = b(100) +//│ ╟── Cannot resolve member 'y' of a value with unknown shape. +//│ ╟── The shape of this value is unknown. +//│ ║ l.126: let c = b(100) //│ ╙── ^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -146,14 +174,30 @@ let b = Baz(123)(1234) :re let c = b(100) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.147: let c = b(100) +//│ ║ l.175: let c = b(100) //│ ║ ^^^^^^ //│ ╟── Instance of class 'Baz' cannot receive more argument lists. -//│ ║ l.142: let b = Baz(123)(1234) +//│ ║ l.170: let b = Baz(123)(1234) //│ ╙── ^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ c = undefined +// An invalid call contributes an unknown result, not another constructor instance. +:ge :re [c.x, c.y] -//│ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'x') +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.188: [c.x, c.y] +//│ ║ ^^^ +//│ ╟── Cannot resolve member 'x' of a value with unknown shape. +//│ ╟── The shape of this value is unknown. +//│ ║ l.175: let c = b(100) +//│ ╙── ^^^^^^ +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.188: [c.x, c.y] +//│ ║ ^^^ +//│ ╟── Cannot resolve member 'y' of a value with unknown shape. +//│ ╟── The shape of this value is unknown. +//│ ║ l.175: let c = b(100) +//│ ╙── ^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls index ecd6fffe3e..890e5f03f2 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugDestinations.mls @@ -30,7 +30,7 @@ fs.readFileSync(debugFile, "utf8") == "Lowering.term 42\nLowering.term 45\n" //│ Lowering.term 47 //│ Lowering.term undefined //│ avoidNames Scoped(Set(member:forced_output⁰),Define(FunDefn(None,member:forced_output⁰,term:forced_output¹,List... -//│ | avoidNames Define(FunDefn(None,member:forced_output⁰,term:forced_output¹,List(ParamList(‹›,List(),None)),Return... +//│ | avoidNames Define(FunDefn(None,member:forced_output⁰,term:forced_output¹,List(LoweredParamList(‹›,List(),None))... //│ | | avoidNames Return(Lit(IntLit(47)))... //│ | | avoidNames End()... diff --git a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls index 9bc54ecb7a..03dbc78736 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DebugScoping.mls @@ -39,7 +39,7 @@ DebugMethods.selected(7) //│ Lowering.term 1601 //│ Lowering.term undefined //│ avoidNames Scoped(Set(member:command_lowering⁰),Define(FunDefn(None,member:command_lowering⁰,term:command_lower... -//│ | avoidNames Define(FunDefn(None,member:command_lowering⁰,term:command_lowering¹,List(ParamList(‹›,List(),None)),... +//│ | avoidNames Define(FunDefn(None,member:command_lowering⁰,term:command_lowering¹,List(LoweredParamList(‹›,List(),... //│ | | avoidNames Return(Lit(IntLit(1601)))... //│ | | avoidNames End()... diff --git a/hkmc2/shared/src/test/mlscript/codegen/DelayedLetInit.mls b/hkmc2/shared/src/test/mlscript/codegen/DelayedLetInit.mls index 5552c51894..ad30ca54cd 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/DelayedLetInit.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/DelayedLetInit.mls @@ -64,7 +64,7 @@ fun f() = 1 //│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— //│ let f; //│ f = function f() { -//│ runtime.checkArgs("f", 0, true, arguments.length); +//│ runtime.checkArgs("f", 0, arguments.length); //│ return 1 //│ }; //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— @@ -81,7 +81,7 @@ f(x) = x + 1 //│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— //│ let f1, f2; //│ f2 = function f(x1) { -//│ runtime.checkArgs("f", 1, true, arguments.length); +//│ runtime.checkArgs("f", 1, arguments.length); //│ return x1 + 1 //│ }; //│ f1 = f2; @@ -144,7 +144,7 @@ fun f() = //│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— //│ let f3; //│ f3 = function f() { -//│ runtime.checkArgs("f", 0, true, arguments.length); +//│ runtime.checkArgs("f", 0, arguments.length); //│ foo2 = 42; //│ return runtime.Unit //│ }; diff --git a/hkmc2/shared/src/test/mlscript/codegen/Do.mls b/hkmc2/shared/src/test/mlscript/codegen/Do.mls index c1e2256551..feba3a3667 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Do.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Do.mls @@ -40,18 +40,18 @@ val f = case //│ Split with nested patterns: //│ > case //│ > caseScrut is 0 then "null" -//│ > let tmp:unused = member:console⁰^term:f⁰.log("non-null") +//│ > let tmp:unused = member:console⁰^term:f⁰.log‹term:console¹/log⁰›("non-null") //│ > caseScrut is 1 then "unit" //│ > let res = "other" -//│ > let tmp:unused = member:Predef⁰.print^term:f⁰(res⁰) +//│ > let tmp:unused = member:Predef⁰.print‹term:Predef¹/print⁰›^term:f⁰(res⁰) //│ > caseScrut is _ then res⁰ //│ Expanded split with flattened patterns: //│ > ‹if|while› //│ > caseScrut is 0 then "null" -//│ > let tmp:unused = member:console⁰^term:f⁰.log("non-null") +//│ > let tmp:unused = member:console⁰^term:f⁰.log‹term:console¹/log⁰›("non-null") //│ > caseScrut is 1 then "unit" //│ > let res = "other" -//│ > let tmp:unused = member:Predef⁰.print^term:f⁰(res⁰) +//│ > let tmp:unused = member:Predef⁰.print‹term:Predef¹/print⁰›^term:f⁰(res⁰) //│ > else res⁰ //│ f = fun diff --git a/hkmc2/shared/src/test/mlscript/codegen/ErasedTypes.mls b/hkmc2/shared/src/test/mlscript/codegen/ErasedTypes.mls index eb3f1813ec..75cbeb1fe9 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/ErasedTypes.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/ErasedTypes.mls @@ -1,3 +1,4 @@ +:. :global :sir @@ -94,7 +95,8 @@ val n: Boxed = o //│ define Boxed¹ as class Boxed⁰ { //│ private val v⁰: Int⁰; //│ constructor Boxed²(v: Int⁰) { -//│ set Boxed⁰.this.v⁰ = v; +//│ @private +//│ define v¹ as val v⁰: Int⁰ = v; //│ end //│ } //│ }; @@ -151,7 +153,8 @@ fun main = //│ define Foo¹ as class Foo⁰ { //│ private val x⁰: Int⁰; //│ constructor Foo²(x: Int⁰) { -//│ set Foo⁰.this.x⁰ = x; +//│ @private +//│ define x¹ as val x⁰: Int⁰ = x; //│ end //│ } //│ }; @@ -172,7 +175,7 @@ fun main = //│ define mk⁰ as fun mk¹(a: ?): ? { //│ 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⁵(): ? { let made: ?; set made = mk¹(1); return made.y¹ }; //│ end @@ -220,17 +223,17 @@ class Cell(mut val v: Int) with fun whatever(a) = a //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define Cell⁰ as class Cell² { -//│ val v¹: Int⁰; +//│ val v²: Int⁰; //│ constructor Cell¹(v: Int⁰) { -//│ define v¹ as val v²: Int⁰ = v; +//│ define v² as val v³: Int⁰ = v; //│ end //│ } //│ method put⁰ = fun put¹(x: ?): ? { -//│ set Cell².this.v² = (x as! Int⁰); +//│ set Cell².this.v³ = (x as! Int⁰); //│ return runtime⁰.Unit⁰ //│ } //│ method get⁰ = fun get¹(): Int⁰ { -//│ return Cell².this.v² +//│ return Cell².this.v³ //│ } //│ method twice⁰ = fun twice¹(x: Int⁰): Int⁰ { //│ return (+⁰(x, x) as! Int⁰) @@ -326,16 +329,16 @@ 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.324: useObj(i) +//│ ║ l.327: useObj(i) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Object' at an unrelated type 'Int' -//│ ║ l.325: useInt(o) +//│ ║ l.328: useInt(o) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int32' at an unrelated type 'Object' -//│ ║ l.326: useObj(x) +//│ ║ l.329: useObj(x) //│ ╙── ^ //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Object' at an unrelated type 'Int32' -//│ ║ l.327: useInt32(o) +//│ ║ l.330: useInt32(o) //│ ╙── ^ //│ ═══[COMPILATION ERROR] Cannot use a value of type 'Int32' at an unrelated type 'Unknown' //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— @@ -362,7 +365,7 @@ 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.363: fun f(x: Int32) = id(x) +//│ ║ l.366: fun f(x: Int32) = id(x) //│ ╙── ^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define id⁰ as fun id¹(x: Unknown): ? { @@ -399,7 +402,7 @@ 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.400: fun bad(x: Shape) = useShape(x) +//│ ║ l.403: fun bad(x: Shape) = useShape(x) //│ ╙── ^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define useShape⁰ as fun useShape¹(x: Shape⁰): ? { @@ -451,8 +454,12 @@ fun widen(f: Foo): FooAlias = f //│ return f //│ }; //│ define Foo⁴ as class Foo³ { -//│ private val x¹: Int⁰; -//│ constructor Foo⁵(x: Int⁰) { set Foo³.this.x¹ = x; end } +//│ private val x²: Int⁰; +//│ constructor Foo⁵(x: Int⁰) { +//│ @private +//│ define x³ as val x²: Int⁰ = x; +//│ end +//│ } //│ }; //│ define w⁰ as val w¹: Int⁰ = 2; //│ end @@ -473,12 +480,22 @@ fun useMyI32(x: MyI32): Int32 = useI32(x) // An alias whose right-hand side does not resolve to a single type symbol behaves as the top type during // hierarchy analysis. A cycle is such a case; a union is not, since it erases to its members' LUB. +:e type Cyclic = Cyclic class P class Q 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 +//│ ║ ^^^^^^ +//│ ╟── Unguarded recursive type alias 'Cyclic'. +//│ ║ l.484: type Cyclic = Cyclic +//│ ║ ^^^^^^ +//│ ╟── Recursive references must pass through a record, tuple, function, or nominal type. +//│ ║ l.484: type Cyclic = Cyclic +//│ ╙── ^^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define useCyclic⁰ as fun useCyclic¹(x: Unknown): Unknown { //│ return x @@ -558,7 +575,7 @@ 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.559: fun intoMixed(n: Int32) = mixed(n) +//│ ║ l.576: fun intoMixed(n: Int32) = mixed(n) //│ ╙── ^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define intoMixed⁰ as fun intoMixed¹(n: Int32): ? { return mixed¹(n) }; end @@ -807,7 +824,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.808: val bad: Str = thunk +//│ ║ l.825: val bad: Str = thunk //│ ╙── ^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let tmp: Function⁰; set tmp = thunk¹(); define bad² as val bad³: Str⁰ = tmp; end @@ -819,7 +836,7 @@ 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.820: val notCalled: Str = called +//│ ║ l.837: val notCalled: Str = called //│ ╙── ^^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define called⁰ as fun called¹(): ? { @@ -853,7 +870,7 @@ fun useMember: Int = M.memberGetter // Applying the getter applies its *result*, which its signature says nothing about, so the call is untyped // and returning it from `: Int` needs a cast. Were the call read as the getter's own auto-invocation, this // would silently take the getter's result type and the cast would be dropped. -fun callMember: Int = M.memberGetter() +fun callMember: Int = M.memberGetter{as Function}() //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define callMember⁰ as fun callMember¹(): Int⁰ { return (M².memberGetter¹() as! Int⁰) }; end @@ -863,7 +880,7 @@ fun callMember: Int = M.memberGetter() :ge fun badInst() = useInt(new Bar("x")) //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Bar' at an unrelated type 'Int' -//│ ║ l.864: fun badInst() = useInt(new Bar("x")) +//│ ║ l.881: fun badInst() = useInt(new Bar("x")) //│ ╙── ^^^^^^^^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define badInst⁰ as fun badInst¹(): ? { @@ -880,9 +897,12 @@ fun test: Function = id //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define test² as fun test³(): Function⁰ { return id¹ }; end -fun test: Function = new Object +fun test: Function = new Object() //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— -//│ define test⁴ as fun test⁵(): Function⁰ { return (new globalThis⁰.Object⁰() as! Function⁰) }; end +//│ define test⁴ as fun test⁵(): Function⁰ { +//│ return (new globalThis⁰.Object⁰(undefined) as! Function⁰) +//│ }; +//│ end // A written arrow type erases to `Function` in every position it can be written - parameter, result and `val` @@ -920,7 +940,7 @@ val g: Int -> Int = (x: Int) => x :ge fun badFn() = applyF(42) //│ ╔══[COMPILATION ERROR] Cannot use a value of type 'Int' at an unrelated type 'Function' -//│ ║ l.921: fun badFn() = applyF(42) +//│ ║ l.941: fun badFn() = applyF(42) //│ ╙── ^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define badFn⁰ as fun badFn¹(): ? { return applyF¹(42) }; end @@ -949,7 +969,7 @@ 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.950: fun badAdd() = useStr(addInts(1, 2)) +//│ ║ l.970: fun badAdd() = useStr(addInts(1, 2)) //│ ╙── ^^^^^^^^^^^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ define badAdd⁰ as fun badAdd¹(): ? { @@ -1244,7 +1264,8 @@ class Box(content: Int) with //│ define Box¹ as class Box⁰ { //│ private val content⁰: Int⁰; //│ constructor Box²(content: Int⁰) { -//│ set Box⁰.this.content⁰ = content; +//│ @private +//│ define content¹ as val content⁰: Int⁰ = content; //│ end //│ } //│ method unwrap⁰ = fun unwrap¹(): ? { return peek⁰(Box⁰.this) } @@ -1262,7 +1283,7 @@ fun tagged(x: Int) = fun describe() = Tag.label() describe() //│ ╔══[WARNING] Objects are not yet lifted. -//│ ║ l.1260: object Tag with +//│ ║ l.1281: object Tag with //│ ╙── ^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let describe; @@ -1289,7 +1310,7 @@ fun mk() = fun get() = Tag.label() new Holder //│ ╔══[WARNING] Objects are not yet lifted. -//│ ║ l.1286: object Tag with +//│ ║ l.1307: object Tag with //│ ╙── ^^^ //│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— //│ let Holder; diff --git a/hkmc2/shared/src/test/mlscript/codegen/FieldSymbols.mls b/hkmc2/shared/src/test/mlscript/codegen/FieldSymbols.mls index d85258ddbf..d2a30d492b 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/FieldSymbols.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/FieldSymbols.mls @@ -67,7 +67,7 @@ case //│ form = ReturningIf //│ split = Cons: //│ branch = Match: -//│ scrutinee = Ref{sym=caseScrut⁰} of caseScrut⁰ +//│ scrutinee = SimpleRef of caseScrut⁰ //│ pattern = Constructor: //│ target = MemberRef of member:Foo⁰ //│ arguments = S of Ls of diff --git a/hkmc2/shared/src/test/mlscript/codegen/Functions.mls b/hkmc2/shared/src/test/mlscript/codegen/Functions.mls index 4240a29969..056a208a69 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Functions.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Functions.mls @@ -91,7 +91,7 @@ fun test2(y) = y + 1 //│ REPL> Sending: block$res18 = undefined //│ REPL> Collected: //│ > undefined -//│ REPL> Sending: let test2, test1;try { test1 = function test1(x2) { runtime.checkArgs("test1", 1, true, arguments.length); return x2 + 1 }; test2 = function test2(y2) { runtime.checkArgs("test2", 1, true, arguments.length); return y2 + 1 };; undefined } catch (e) { console.log('\u200B' + (e.stack ?? e) + '\u200B'); } +//│ REPL> Sending: let test2, test1;try { test1 = function test1(x2) { runtime.checkArgs("test1", 1, arguments.length); return x2 + 1 }; test2 = function test2(y2) { runtime.checkArgs("test2", 1, arguments.length); return y2 + 1 };; undefined } catch (e) { console.log('\u200B' + (e.stack ?? e) + '\u200B'); } //│ REPL> Collected: //│ > undefined //│ REPL> Parsed: diff --git a/hkmc2/shared/src/test/mlscript/codegen/InlineLambdas.mls b/hkmc2/shared/src/test/mlscript/codegen/InlineLambdas.mls index 05beb46b73..028d2e9e07 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/InlineLambdas.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/InlineLambdas.mls @@ -40,13 +40,13 @@ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let lambda2; lambda2 = (undefined, function (x) { return x }); return lambda2 + 1 //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ = "function (x) { runtime.checkArgs(\"\", 1, true, arguments.length); return x }1" +//│ = "function (x) { runtime.checkArgs(\"\", 1, arguments.length); return x }1" :sjs 1 + (x => x) //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let lambda3; lambda3 = (undefined, function (x) { return x }); return 1 + lambda3 //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ = "1function (x) { runtime.checkArgs(\"\", 1, true, arguments.length); return x }" +//│ = "1function (x) { runtime.checkArgs(\"\", 1, arguments.length); return x }" diff --git a/hkmc2/shared/src/test/mlscript/codegen/Juxtapositions.mls b/hkmc2/shared/src/test/mlscript/codegen/Juxtapositions.mls index 440514f9cf..ebd52643d0 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Juxtapositions.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Juxtapositions.mls @@ -49,7 +49,7 @@ add(2) //│ ╔══[WARNING] Pure expression in statement position //│ ║ l.41: 42 //│ ╙── ^^ -//│ ═══[RUNTIME ERROR] Error: Function 'add' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'add' expected at least 2 arguments but got 1 42 add(2) + 1 @@ -232,7 +232,7 @@ inc() //│ ╟── Function 'inc' expected 1 argument, but got 0 //│ ║ l.15: fun inc(x) = x + 1 //│ ╙── ^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'inc' expected 1 argument but got 0 +//│ ═══[RUNTIME ERROR] Error: Function 'inc' expected at least 1 argument but got 0 :e :re @@ -246,7 +246,7 @@ inc() //│ ╟── Function 'inc' expected 1 argument, but got 0 //│ ║ l.15: fun inc(x) = x + 1 //│ ╙── ^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'inc' expected 1 argument but got 0 +//│ ═══[RUNTIME ERROR] Error: Function 'inc' expected at least 1 argument but got 0 42 diff --git a/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls b/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls index 8f63e91ad5..203e27bf1c 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/PartialApps.mls @@ -106,7 +106,7 @@ i(print)(2) //│ ╟── Function literal expected 2 arguments, but got 1 //│ ║ l.91: let i = _(0, 1, _) //│ ╙── ^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'i' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'i' expected at least 2 arguments but got 1 // :re i((x, y, z) => x + y + z, 2) @@ -119,12 +119,18 @@ i((...args) => [...args], _)(2) //│ ║ 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) //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── 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, _) //│ ╙── ^^^^^^^^^ //│ = fun @@ -143,14 +149,17 @@ i((...args) => [...args], _)(2) :e :re 123 |> (_ + 1 >> _ * 2) -//│ ╔══[COMPILATION ERROR] Resolution error in function type -//│ ║ l.30: fun (|>) pipeInto[A, B](x: A, f: A -> B): B = f(x) -//│ ║ ^^^^^^ +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.151: 123 |> (_ + 1 >> _ * 2) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Callback parameter list does not match its declared function type. -//│ ║ in PartialApps.mls:145 -//│ ║ l.145: 123 |> (_ + 1 >> _ * 2) -//│ ╙── ^^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function expected 2 arguments but got 1 +//│ ║ l.151: 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) +//│ ╙── ^^^^^^ +//│ ═══[RUNTIME ERROR] Error: Function expected at least 2 arguments but got 1 (_ + 1 + _ * 2) of 123, 456 //│ = 1036 @@ -239,7 +248,7 @@ _ - _ of 1, 2 :pe |> 1 //│ ╔══[PARSE ERROR] Expected end of input; found literal instead -//│ ║ l.240: |> 1 +//│ ║ l.249: |> 1 //│ ╙── ^ //│ = fun pipeInto @@ -247,7 +256,7 @@ _ - _ of 1, 2 :re |> (1) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.248: |> (1) +//│ ║ l.257: |> (1) //│ ║ ^^^^^^ //│ ╟── Function 'pipeInto' expected 2 arguments, but got 1 //│ ║ in ../../mlscript-compile/Predef.mls @@ -296,10 +305,10 @@ _ - _ of 1, 2 :w let f = if _ then 1 else 0 //│ ╔══[WARNING] This catch-all clause makes the following branches unreachable. -//│ ║ l.297: let f = if _ then 1 else 0 +//│ ║ l.306: let f = if _ then 1 else 0 //│ ║ ^^^^^^ //│ ╟── This branch is unreachable. -//│ ║ l.297: let f = if _ then 1 else 0 +//│ ║ l.306: let f = if _ then 1 else 0 //│ ╙── ^^^^^^ //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let f23; f23 = 1; diff --git a/hkmc2/shared/src/test/mlscript/codegen/PrivateMembers.mls b/hkmc2/shared/src/test/mlscript/codegen/PrivateMembers.mls index cbc5cabb0a..6f8ce4a076 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/PrivateMembers.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/PrivateMembers.mls @@ -31,7 +31,7 @@ class Foo with //│ runtime.checkCall(Predef.print(tmp2)); //│ } //│ #y() { -//│ runtime.checkArgs("y", 0, true, arguments.length); +//│ runtime.checkArgs("y", 0, arguments.length); //│ return 42 //│ } //│ toString() { return runtime.render(this); } diff --git a/hkmc2/shared/src/test/mlscript/codegen/Repl.mls b/hkmc2/shared/src/test/mlscript/codegen/Repl.mls index 7deec19bb2..7b75f2df8e 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Repl.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Repl.mls @@ -11,7 +11,7 @@ fun res() = 1 //│ REPL> Sending: block$res3 = undefined //│ REPL> Collected: //│ > undefined -//│ REPL> Sending: let res2;try { res2 = function res() { runtime.checkArgs("res", 0, true, arguments.length); return 1 };; undefined } catch (e) { console.log('\u200B' + (e.stack ?? e) + '\u200B'); } +//│ REPL> Sending: let res2;try { res2 = function res() { runtime.checkArgs("res", 0, arguments.length); return 1 };; undefined } catch (e) { console.log('\u200B' + (e.stack ?? e) + '\u200B'); } //│ REPL> Collected: //│ > undefined //│ REPL> Parsed: diff --git a/hkmc2/shared/src/test/mlscript/codegen/RuntimeUsage.mls b/hkmc2/shared/src/test/mlscript/codegen/RuntimeUsage.mls index e2d02d1b69..fd893083cb 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/RuntimeUsage.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/RuntimeUsage.mls @@ -33,7 +33,7 @@ print of safeCall(123) print of safeCall(null) //│ > null -print of safeCall(undefined) +print of safeCall(unsafe.undefined) //│ > () print of safeCall(()) @@ -46,7 +46,7 @@ print of checkCall(null) //│ > null :re -print of checkCall(undefined) +print of checkCall(unsafe.undefined) //│ ═══[RUNTIME ERROR] Error: MLscript call unexpectedly returned `undefined`, the forbidden value. diff --git a/hkmc2/shared/src/test/mlscript/codegen/SanityChecks.mls b/hkmc2/shared/src/test/mlscript/codegen/SanityChecks.mls index 6ca71298ed..1bed4c0a85 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/SanityChecks.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/SanityChecks.mls @@ -11,7 +11,7 @@ f(2, 3) //│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— //│ let f; //│ f = function f(x, y) { -//│ runtime.checkArgs("f", 2, true, arguments.length); +//│ runtime.checkArgs("f", 2, arguments.length); //│ return x + y //│ }; //│ block$res2 = 5; @@ -25,7 +25,7 @@ f(2, 3) //│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— //│ let f1, f2; //│ f2 = function f(x, y) { -//│ runtime.checkArgs("f", 2, true, arguments.length); +//│ runtime.checkArgs("f", 2, arguments.length); //│ return x + y //│ }; //│ f1 = f2; @@ -36,11 +36,11 @@ f(2, 3) :re unchecked(f)(2) -//│ ═══[RUNTIME ERROR] Error: Function 'f' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'f' expected at least 2 arguments but got 1 -:re +// * Extra arguments are ignored, as in JavaScript unchecked(f)(2, 3, 4) -//│ ═══[RUNTIME ERROR] Error: Function 'f' expected 2 arguments but got 3 +//│ = 5 :ssjs @@ -60,7 +60,7 @@ unchecked(f)(2) //│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— //│ block$res7 = runtime.safeCall(f1(2)); //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ ═══[RUNTIME ERROR] Error: Function 'f' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'f' expected at least 2 arguments but got 1 :ssjs @@ -70,7 +70,7 @@ unchecked(f)(3)(4) //│ —————————————| JS (sanitized) |————————————————————————————————————————————————————————————————————— //│ let f3; //│ f3 = function f(x) { -//│ runtime.checkArgs("f", 1, true, arguments.length); +//│ runtime.checkArgs("f", 1, arguments.length); //│ return (y, z) => { //│ let tmp; //│ tmp = x + y; @@ -92,7 +92,7 @@ let f = (x, y) => x + y in unchecked(f)(2) :re let f = (x, y) => x + y unchecked(f)(2) -//│ ═══[RUNTIME ERROR] Error: Function 'f' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'f' expected at least 2 arguments but got 1 //│ f = fun f @@ -109,7 +109,7 @@ unchecked(Cls(1, 2)).f(3) data class Cls(x, y) with fun f(z, p) = x + y + z + p unchecked(Cls(1, 2)).f(3) -//│ ═══[RUNTIME ERROR] Error: Function 'f' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'f' expected at least 2 arguments but got 1 :ssjs @@ -133,7 +133,7 @@ unchecked(Cls(1, 2)).f(3, 4)(5) //│ } //│ get f$__checkNotMethod() { runtime.deboundMethod("f", "Cls"); } //│ f(z, p) { -//│ runtime.checkArgs("f", 2, true, arguments.length); +//│ runtime.checkArgs("f", 2, arguments.length); //│ return (q, s) => { //│ let tmp3, tmp4, tmp5, tmp6; //│ tmp3 = this.x + this.y; diff --git a/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls b/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls index 6e2819f846..5a967bd0af 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Spreads.mls @@ -33,7 +33,7 @@ foo(...a) //│ ╟── Function 'foo' expected 4 arguments, but got 3 //│ ║ l.12: fun foo(w, x, y, z) = [w, x, y, z] //│ ╙── ^^^^^^^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'foo' expected 4 arguments but got 3 +//│ ═══[RUNTIME ERROR] Error: Function 'foo' expected at least 4 arguments but got 3 let f = (...xs) => xs @@ -108,15 +108,16 @@ new A(1, 2, 3).getR //│ ═══[RUNTIME ERROR] ReferenceError: r is not defined //│ ═══[RUNTIME ERROR] Expected: '[1, 2, 3]', got: 'undefined' -:todo fun (||>) pipeIntoMulti[A, B](...xs: Array[A], f: (...Array[A]) -> B): B = f(...xs) -//│ ╔══[COMPILATION ERROR] Spread parameters must be the last in the parameter list. -//│ ║ l.112: fun (||>) pipeIntoMulti[A, B](...xs: Array[A], f: (...Array[A]) -> B): B = f(...xs) -//│ ╙── ^^^^^^^^^^^^^^^ -//│ /!!!\ Uncaught error: scala.NotImplementedError: variance traversal of spread elements (of class String) -:todo +:expect 6 +pipeIntoMulti(1, 2, 3, (...xs) => xs.0 + xs.1 + xs.2) +//│ = 6 + fun (<||) pipeFromMulti[A, B](f: (...Array[A]) -> B, ...xs: Array[A]): B = f(...xs) -//│ /!!!\ Uncaught error: scala.NotImplementedError: variance traversal of spread elements (of class String) + +:expect 6 +pipeFromMulti((...xs) => xs.0 + xs.1 + xs.2, 1, 2, 3) +//│ = 6 diff --git a/hkmc2/shared/src/test/mlscript/codegen/Throw.mls b/hkmc2/shared/src/test/mlscript/codegen/Throw.mls index 4e57453244..d4726f90dd 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/Throw.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/Throw.mls @@ -34,9 +34,9 @@ f(1) //│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— //│ let f2; //│ f2 = function f(x) { -//│ throw globalThis.Object.freeze(new globalThis.Error("e")) +//│ throw globalThis.Object.freeze(new globalThis.Error("e", undefined)) //│ }; -//│ throw globalThis.Object.freeze(new globalThis.Error("e")) +//│ throw globalThis.Object.freeze(new globalThis.Error("e", undefined)) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ ═══[RUNTIME ERROR] Error: e @@ -50,14 +50,14 @@ f(false) //│ let f3; //│ f3 = function f(x) { //│ if (x === true) { -//│ throw globalThis.Object.freeze(new globalThis.Error("x")) +//│ throw globalThis.Object.freeze(new globalThis.Error("x", undefined)) //│ } -//│ throw globalThis.Object.freeze(new globalThis.Error("y")); +//│ throw globalThis.Object.freeze(new globalThis.Error("y", undefined)); //│ }; //│ if (false === true) { -//│ throw globalThis.Object.freeze(new globalThis.Error("x")) +//│ throw globalThis.Object.freeze(new globalThis.Error("x", undefined)) //│ } -//│ throw globalThis.Object.freeze(new globalThis.Error("y")); +//│ throw globalThis.Object.freeze(new globalThis.Error("y", undefined)); //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ ═══[RUNTIME ERROR] Error: y diff --git a/hkmc2/shared/src/test/mlscript/codegen/TraceLog.mls b/hkmc2/shared/src/test/mlscript/codegen/TraceLog.mls index 80735a4ae3..5bee054131 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/TraceLog.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/TraceLog.mls @@ -134,3 +134,10 @@ id(2) //│ > => 0 //│ = 0 + +// * All the parameters are shown, including variadic and trailing ones. +fun variadic(x, y?, ...xs, z) = x +variadic(1, 2, 3, 4, 5) +//│ > CALL variadic(1, 2, ...[ 3, 4 ], 5) +//│ > => 1 +//│ = 1 diff --git a/hkmc2/shared/src/test/mlscript/codegen/TraceLogIndent.mls b/hkmc2/shared/src/test/mlscript/codegen/TraceLogIndent.mls index 9cec7c72e0..1e0271be59 100644 --- a/hkmc2/shared/src/test/mlscript/codegen/TraceLogIndent.mls +++ b/hkmc2/shared/src/test/mlscript/codegen/TraceLogIndent.mls @@ -33,7 +33,7 @@ h(3) //│ > | | CALL g(2) //│ > | | | CALL g(1) //│ > | | | | CALL g(0) -//│ ═══[RUNTIME ERROR] Error: Function 'plus' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'plus' expected at least 2 arguments but got 1 :traceJS h(-2) @@ -60,7 +60,7 @@ f(1,2)(3) //│ > | CALL g(2) //│ > | | CALL g(1) //│ > | | | CALL g(0) -//│ ═══[RUNTIME ERROR] Error: Function 'plus' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'plus' expected at least 2 arguments but got 1 :showRepl :traceJS diff --git a/hkmc2/shared/src/test/mlscript/ctx/ExplicitlySpec.mls b/hkmc2/shared/src/test/mlscript/ctx/ExplicitlySpec.mls index c0577a3a0f..3953533a6a 100644 --- a/hkmc2/shared/src/test/mlscript/ctx/ExplicitlySpec.mls +++ b/hkmc2/shared/src/test/mlscript/ctx/ExplicitlySpec.mls @@ -15,7 +15,7 @@ f(using 42) //│ ╔══[COMPILATION ERROR] Expected 2 arguments, got 1 //│ ║ l.14: f(using 42) //│ ╙── ^^ -//│ ═══[RUNTIME ERROR] Error: Function 'f' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'f' expected at least 2 arguments but got 1 :expect 45.14 f(using 42, 3.14) diff --git a/hkmc2/shared/src/test/mlscript/dead-param-elim/dead-ref.mls b/hkmc2/shared/src/test/mlscript/dead-param-elim/dead-ref.mls index 62a9db3326..f545cd45a8 100644 --- a/hkmc2/shared/src/test/mlscript/dead-param-elim/dead-ref.mls +++ b/hkmc2/shared/src/test/mlscript/dead-param-elim/dead-ref.mls @@ -10,7 +10,7 @@ private fun f(a) = a = 3 g(a) private fun g(a) = 2 -f(undefined) +f(unsafe.undefined) //│ dead-param-elim > >>> dead-param-elim results >>> //│ dead-param-elim > f⁰#0 @ -> eliminable: {0} //│ dead-param-elim > g⁰#0 @ -> eliminable: {0} diff --git a/hkmc2/shared/src/test/mlscript/decls/Prelude.mls b/hkmc2/shared/src/test/mlscript/decls/Prelude.mls index 9fc833ddfb..5fe0270fb4 100644 --- a/hkmc2/shared/src/test/mlscript/decls/Prelude.mls +++ b/hkmc2/shared/src/test/mlscript/decls/Prelude.mls @@ -5,12 +5,13 @@ declare type Anything declare type Nothing declare open class Object { + constructor(value?) fun toString(): Str } -declare fun Object(...args): Object // TODO: use an optional parameter when supported +declare fun Object(value?): Object declare module Object { fun entries(target): Array[[Str, Any]] - fun getOwnPropertyDescriptor(target, key): Object | undefined + fun getOwnPropertyDescriptor(target, key): Object | unsafe.undefined fun getOwnPropertyDescriptors(target): Object fun create @@ -28,9 +29,11 @@ declare module JSON { stringify } -declare class Number: Num +declare class Number: Num { + constructor(value?) +} // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/Number -declare fun Number(...args): Number // TODO: use an optional parameter when supported +declare fun Number(value?): Number declare module Number { val MIN_VALUE: Num @@ -49,17 +52,19 @@ declare class BigInt { declare fun BigInt(value: Str | Int | Bool | BigInt): BigInt declare module BigInt -// TODO: declare as class Function[...As, B] -declare open class Function { +// TODO: support bounds on inline type parameters like `extends Array` +declare open class Function[A /* extends Array */, B]: ((...A) -> B) { val name: Str fun bind(thisArg, ...args): Function - fun apply(thisArg, args: Array): Any + fun apply(thisArg, args: A): B } declare module Function -declare class String +declare class String { + constructor(value?) +} // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/String -declare fun String(...args): Str // TODO: use an optional parameter when supported +declare fun String(value?): Str declare module String { fun fromCharCode @@ -68,6 +73,7 @@ declare module String { } declare class RegExp { + constructor(source?: Str | RegExp, flags?: Str) fun toString(): Str fun exec(input: Str): Array[Str] | null fun test(input: Str): Bool @@ -75,33 +81,39 @@ declare class RegExp { declare module RegExp declare class Set[V] { + constructor(values?: Iterable[V] | null) + @annotations.jsSymbol(Symbol.iterator) fun iterator(): IterableIterator[V] val size: Int fun add(value: V): Set[V] fun has(value: V): Bool fun delete(value: V): Bool - fun clear(): undefined - fun values(): Iterator[V] + fun clear(): unsafe.undefined + fun values(): IterableIterator[V] fun union(other: Set[V]): Set[V] fun difference(other: Set[V]): Set[V] } declare module Set declare class Map[K, V] { + constructor(entries?: Iterable[[K, V]] | null) + @annotations.jsSymbol(Symbol.iterator) fun iterator(): IterableIterator[[K, V]] val size: Int - fun get(key: K): V | undefined + fun get(key: K): V | unsafe.undefined fun has(key: K): Bool fun delete(key: K): Bool - fun clear(): undefined + fun clear(): unsafe.undefined fun id"set"(key: K, value: V): Map[K, V] - fun keys(): Iterator[K] - fun values(): Iterator[V] - fun entries(): Iterator[[K, V]] - fun forEach(f: (V, K, Map[K, V]) -> Any): undefined + fun keys(): IterableIterator[K] + fun values(): IterableIterator[V] + fun entries(): IterableIterator[[K, V]] + fun forEach(f: (V, drop K, drop Map[K, V]) -> Any): unsafe.undefined } declare class Iterator[out T] { fun next(): { value: T, done: Bool } } +type Iterable[out A] = {@annotations.jsSymbol(Symbol.iterator) iterator: () -> Iterator[A]} +type IterableIterator[out A] = Iterator[A] & Iterable[A] declare module Map @@ -109,7 +121,7 @@ declare class WeakSet[V] declare module WeakSet declare class WeakMap[K, V] { - fun get(key: K): V | undefined + fun get(key: K): V | unsafe.undefined fun has(key: K): Bool fun delete(key: K): Bool fun id"set"(key: K, value: V): WeakMap[K, V] @@ -117,20 +129,25 @@ declare class WeakMap[K, V] { declare module WeakMap declare class Error { + constructor(message?, options?) val message: Str val name: Str - val stack: Str | undefined + val stack: Str | unsafe.undefined } // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/Error -declare fun Error(...args): Error // TODO: use optional parameters when supported +declare fun Error(message?, options?): Error // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError/TypeError -declare class TypeError extends Error -declare fun TypeError(...args): TypeError +declare class TypeError extends Error(message, options) { + constructor(message?, options?) +} +declare fun TypeError(message?, options?): TypeError // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError/RangeError -declare class RangeError extends Error -declare fun RangeError(...args): RangeError +declare class RangeError extends Error(message, options) { + constructor(message?, options?) +} +declare fun RangeError(message?, options?): RangeError declare class Date declare module Date @@ -144,7 +161,10 @@ declare module TypedArray declare class Int8Array declare module Int8Array -declare class Uint8Array +declare class Uint8Array { + // The first argument can be a length, an array-like/iterable, or a buffer. + constructor(lengthOrValuesOrBuffer?, byteOffset?: Int, length?: Int) +} declare module Uint8Array declare class Uint8ClampedArray @@ -180,13 +200,12 @@ declare module BigUint64Array // Awaited recursively unwraps promises, as JavaScript's promise resolution does. declare type Awaited[out A] // Follow TypeScript's Promise declarations, translating void returns to undefined. -// Optional parameters and default type arguments are not supported yet: callers -// must supply the first callback, and then's rejection callback uses a rest list. +// Default type arguments are not supported yet: callers must supply the first callback. // Structural PromiseLike values and tuple-mapped all results remain unsupported. -declare class Promise[out A](executor: ((A | Promise[A]) -> undefined, Any -> undefined) -> undefined) { - fun id"then"[B, C](onFulfilled: Awaited[A] -> (B | Promise[B]), ...onRejected: Array[(Any -> (C | Promise[C])) | undefined | null]): Promise[Awaited[B | C]] - fun id"catch"[B](onRejected: (Any -> (B | Promise[B])) | undefined | null): Promise[Awaited[A | B]] - fun id"finally"(onFinally: (() -> undefined) | undefined | null): Promise[A] +declare class Promise[out A](executor: ((A | Promise[A]) -> unsafe.undefined, Any -> unsafe.undefined) -> unsafe.undefined) { + fun id"then"[B, C](onFulfilled: Awaited[A] -> (B | Promise[B]), onRejected?: (Any -> (C | Promise[C])) | null): Promise[Awaited[B | C]] + fun id"catch"[B](onRejected: (Any -> (B | Promise[B])) | unsafe.undefined | null): Promise[Awaited[A | B]] + fun id"finally"(onFinally: (() -> unsafe.undefined) | unsafe.undefined | null): Promise[A] } declare module Promise { fun resolve[A](value: A | Promise[A]): Promise[Awaited[A]] @@ -204,12 +223,13 @@ declare class Num { } declare class Int extends Num declare class Str { + @annotations.jsSymbol(Symbol.iterator) fun iterator(): IterableIterator[Str] val length: Int fun toString(): Str - fun at(index: Int): Str | undefined + fun at(index: Int): Str | unsafe.undefined fun charAt(index: Int): Str fun charCodeAt(index: Int): Num - fun codePointAt(index: Int): Int | undefined + fun codePointAt(index: Int): Int | unsafe.undefined fun indexOf(value: Str, ...from): Int fun lastIndexOf(value: Str, ...from): Int fun includes(value: Str, ...from): Bool @@ -240,41 +260,42 @@ declare class Class // the `.class` storage used by generated MLscript constructor functions. // The `val length` parameter also supports matching with `Array(length)`. declare class Array[T](val length: Int) { + @annotations.jsSymbol(Symbol.iterator) fun iterator(): IterableIterator[T] fun join(...separators): Str fun reverse(): Array[T] fun sort(...compare): Array[T] fun slice(...bounds): Array[T] fun copyWithin(target: Int, start: Int, ...end): Array[T] - fun entries(): Iterator[[Int, T]] - fun keys(): Iterator[Int] + fun entries(): IterableIterator[[Int, T]] + fun keys(): IterableIterator[Int] fun toReversed(): Array[T] fun toSorted(...compare): Array[T] fun toSpliced(start: Int, ...items): Array[T] fun id"with"(index: Int, value: T): Array[T] - fun at(index: Int): T | undefined + fun at(index: Int): T | unsafe.undefined fun fill(value: T, ...bounds): Array[T] fun splice(start: Int, ...items): Array[T] fun push(...items: Array[T]): Int fun unshift(...items: Array[T]): Int - fun pop(): T | undefined - fun shift(): T | undefined + fun pop(): T | unsafe.undefined + fun shift(): T | unsafe.undefined fun concat(...arrays): Array[Any] - fun map[U](f: (T, Int, Array[T]) -> U): Array[U] - fun forEach(f: (T, Int, Array[T]) -> Any): undefined - fun filter(f: (T, Int, Array[T]) -> Bool): Array[T] - fun find(f: (T, Int, Array[T]) -> Bool, ...thisArg): T | undefined - fun findIndex(f: (T, Int, Array[T]) -> Bool, ...thisArg): Int - fun findLast(f: (T, Int, Array[T]) -> Bool, ...thisArg): T | undefined - fun findLastIndex(f: (T, Int, Array[T]) -> Bool, ...thisArg): Int + fun map[U](f: (T, drop Int, drop Array[T]) -> U): Array[U] + fun forEach(f: (T, drop Int, drop Array[T]) -> Any): unsafe.undefined + fun filter(f: (T, drop Int, drop Array[T]) -> Bool): Array[T] + fun find(f: (T, drop Int, drop Array[T]) -> Bool, ...thisArg): T | unsafe.undefined + fun findIndex(f: (T, drop Int, drop Array[T]) -> Bool, ...thisArg): Int + fun findLast(f: (T, drop Int, drop Array[T]) -> Bool, ...thisArg): T | unsafe.undefined + fun findLastIndex(f: (T, drop Int, drop Array[T]) -> Bool, ...thisArg): Int fun flat(...depth): Array[Any] - fun flatMap[U](f: (T, Int, Array[T]) -> (U | Array[U]), ...thisArg): Array[U] - fun every(f: (T, Int, Array[T]) -> Bool): Bool - fun some(f: (T, Int, Array[T]) -> Bool): Bool + fun flatMap[U](f: (T, drop Int, drop Array[T]) -> (U | Array[U]), ...thisArg): Array[U] + fun every(f: (T, drop Int, drop Array[T]) -> Bool): Bool + fun some(f: (T, drop Int, drop Array[T]) -> Bool): Bool // Without an initial value, the first element becomes the accumulator, so the // accumulator type would also have to include T. Methods cannot be overloaded // by arity, so only the form with an initial value is declared. - fun reduce[U](f: (U, T, Int, Array[T]) -> U, initial: U): U - fun reduceRight[U](f: (U, T, Int, Array[T]) -> U, initial: U): U + fun reduce[U](f: (U, T, drop Int, drop Array[T]) -> U, initial: U): U + fun reduceRight[U](f: (U, T, drop Int, drop Array[T]) -> U, initial: U): U fun indexOf(value: T, ...from): Int fun lastIndexOf(value: T, ...from): Int fun includes(value: T, ...from): Bool @@ -283,7 +304,7 @@ declare class Array[T](val length: Int) { } declare module Array { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from - fun from(...args): Array[Any] // TODO: use optional parameters when supported + fun from(items, mapFn?, thisArg?): Array[Any] fun concat isArray @@ -291,15 +312,16 @@ declare module Array { } declare class Symbol { - val description: Str | undefined + val description: Str | unsafe.undefined } declare fun Symbol(...description): Symbol declare module Symbol { - // The `TermDef` needs `rhs` to be defined to be recognized as `isMLsFun`. + // Implementation note: + // A `TermDef` like `for` needs `rhs` to be defined to be recognized as `isMLsFun`. // Otherwise, it would be wrapped in `runtime.safeCall`, which accesses the // uninitialized `runtime` in the `Rendering` module. - fun for: Str -> Any = () - val iterator: Any + fun for: Str -> Symbol = () + @annotations.fresh val iterator: Symbol } // MLwasm-specific types @@ -408,10 +430,13 @@ declare val Infinity // Wasm support declare object WebAssembly { declare class Instance { + constructor(compiledModule: Module, imports?) val exports: dyn } declare class Memory - declare class Module + declare class Module { + constructor(bytes) + } declare module Module { fun exports @@ -436,6 +461,11 @@ declare module source { file } +// This virtual module exposes host values that ordinary MLscript code avoids. +declare module unsafe { + val id"undefined" +} + declare module js { fun bitand @@ -558,6 +588,8 @@ declare module debug { declare module annotations { object pure + object fresh + fun jsSymbol(path: Symbol) object untyped object tailrec object tailcall @@ -606,6 +638,5 @@ declare open class HTMLElement { // Shared read interfaces for arrays, strings, and lazy views. Element types are // covariant, so using these interfaces does not widen an invariant collection. type Indexable[out A] = {length: Int, at: Int -> A, slice: Function} -type Iterable[out A] = {iterator: () -> Iterator[A]} type IterableIndexable[out A] = Indexable[A] & Iterable[A] diff --git a/hkmc2/shared/src/test/mlscript/deforest/relaxedPrograms.mls b/hkmc2/shared/src/test/mlscript/deforest/relaxedPrograms.mls index 2971e1b5eb..b6ea877ae8 100644 --- a/hkmc2/shared/src/test/mlscript/deforest/relaxedPrograms.mls +++ b/hkmc2/shared/src/test/mlscript/deforest/relaxedPrograms.mls @@ -42,7 +42,7 @@ store.foo(AA(4)) //│ deforest > fields: scrut¹.x¹ //│ deforest > <<< fusing <<< //│ ═══[INTERNAL ERROR] `this` not in scope: class:Store -//│ > let AA3, Store3, store1, tmp2, tmp3, $scrut_AA;try { const n$accessorSymbol = globalThis.Symbol("n"); $scrut_AA = function $scrut_AA(AA_x) { runtime.checkArgs("$scrut_AA", 1, true, arguments.length); return (fv_arg$AA$0$, fv_x) => { return AA_x + ‹MISSING_THIS›[n$accessorSymbol] } }; AA3 = function AA(x) { return globalThis.Object.freeze(new AA.class(x)); }; globalThis.Object.freeze(class AA2 { static { AA3.class = this } constructor(x) { this.x = x; } toString() { return runtime.render(this); } static [definitionMetadata] = ["class", "AA", ["x"]]; }); Store3 = function Store(n) { return globalThis.Object.freeze(new Store.class(n)); }; globalThis.Object.freeze(class Store2 { static { Store3.class = this } constructor(n) { this.#n = n; } #n; get [n$accessorSymbol]() { return this.#n; } set [n$accessorSymbol](value) { this.#n = value; } get get$__checkNotMethod() { runtime.deboundMethod("get", "Store"); } get foo$__checkNotMethod() { runtime.deboundMethod("foo", "Store"); } get() { runtime.checkArgs("get", 0, true, arguments.length); let AA_x; AA_x = this.#n; return runtime.checkCall($scrut_AA(AA_x)(undefined, undefined)) } foo(x) { runtime.checkArgs("foo", 1, true, arguments.length); let arg$AA$0$; if (x instanceof AA3.class) { arg$AA$0$ = x.x; return arg$AA$0$ + this.#n } throw globalThis.Object.freeze(new globalThis.Error("match error")); } toString() { return runtime.render(this); } static [definitionMetadata] = ["class", "Store", [null]]; }); tmp2 = runtime.checkCall(Store3(1)); store1 = tmp2; tmp3 = runtime.checkCall(AA3(4)); block$res3 = runtime.checkCall(store1.foo(tmp3));; undefined } catch (e) { console.log('\u200B' + e + '\u200B'); } +//│ > let AA3, Store3, store1, tmp2, tmp3, $scrut_AA;try { const n$accessorSymbol = globalThis.Symbol("n"); $scrut_AA = function $scrut_AA(AA_x) { runtime.checkArgs("$scrut_AA", 1, arguments.length); return (fv_arg$AA$0$, fv_x) => { return AA_x + ‹MISSING_THIS›[n$accessorSymbol] } }; AA3 = function AA(x) { return globalThis.Object.freeze(new AA.class(x)); }; globalThis.Object.freeze(class AA2 { static { AA3.class = this } constructor(x) { this.x = x; } toString() { return runtime.render(this); } static [definitionMetadata] = ["class", "AA", ["x"]]; }); Store3 = function Store(n) { return globalThis.Object.freeze(new Store.class(n)); }; globalThis.Object.freeze(class Store2 { static { Store3.class = this } constructor(n) { this.#n = n; } #n; get [n$accessorSymbol]() { return this.#n; } set [n$accessorSymbol](value) { this.#n = value; } get get$__checkNotMethod() { runtime.deboundMethod("get", "Store"); } get foo$__checkNotMethod() { runtime.deboundMethod("foo", "Store"); } get() { runtime.checkArgs("get", 0, arguments.length); let AA_x; AA_x = this.#n; return runtime.checkCall($scrut_AA(AA_x)(undefined, undefined)) } foo(x) { runtime.checkArgs("foo", 1, arguments.length); let arg$AA$0$; if (x instanceof AA3.class) { arg$AA$0$ = x.x; return arg$AA$0$ + this.#n } throw globalThis.Object.freeze(new globalThis.Error("match error")); } toString() { return runtime.render(this); } static [definitionMetadata] = ["class", "Store", [null]]; }); tmp2 = runtime.checkCall(Store3(1)); store1 = tmp2; tmp3 = runtime.checkCall(AA3(4)); block$res3 = runtime.checkCall(store1.foo(tmp3));; undefined } catch (e) { console.log('\u200B' + e + '\u200B'); } //│ ═══[COMPILATION ERROR] [Uncaught SyntaxError] Invalid or unexpected token //│ ═══[RUNTIME ERROR] Expected: '5', got: 'undefined' //│ ═══[RUNTIME ERROR] ReferenceError: store1 is not defined diff --git a/hkmc2/shared/src/test/mlscript/examples/.mls b/hkmc2/shared/src/test/mlscript/examples/.mls new file mode 100644 index 0000000000..0954d5f30b --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/examples/.mls @@ -0,0 +1,2 @@ +:.. +:js diff --git a/hkmc2/shared/src/test/mlscript/examples/Greetings.mls b/hkmc2/shared/src/test/mlscript/examples/Greetings.mls new file mode 100644 index 0000000000..2a601760fb --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/examples/Greetings.mls @@ -0,0 +1,46 @@ +:. + + +fun greet(name: Str, greeting?: Str) = + (if greeting is null then "Hello, " else greeting + ", ") + name +print \ greet("Ana") // prints: Hello, Ana +print \ greet("Ana", "Hi") // prints: Hi, Ana +//│ > Hello, Ana +//│ > Hi, Ana + +:todo +fun greet(name: Str, opts?: {greeting?: Str, loud?: Bool}) = ??? +//│ ╔══[COMPILATION ERROR] Unexpected record key shape. +//│ ║ l.12: fun greet(name: Str, opts?: {greeting?: Str, loud?: Bool}) = ??? +//│ ╙── ^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Unexpected record key shape. +//│ ║ l.12: fun greet(name: Str, opts?: {greeting?: Str, loud?: Bool}) = ??? +//│ ╙── ^^^^^ +//│ ╔══[COMPILATION ERROR] Resolution error in record literal +//│ ║ l.12: fun greet(name: Str, opts?: {greeting?: Str, loud?: Bool}) = ??? +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── A record type requires statically named fields. +//│ ║ l.12: fun greet(name: Str, opts?: {greeting?: Str, loud?: Bool}) = ??? +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +fun add(x, ...xs) = xs.reduce(=> _ + _, x) + +fun greet(name, opts?) = + add + \ if opts is {:greeting} then greeting + ", " else "Hello, " + \ name + \ if opts is {:loud} and loud then "!" else "" + +print \ greet("Ana") // prints: Hello, Ana +print \ greet("Ana", greeting: "Hi") // prints: Hi, Ana +print \ greet("Ana", loud: true) // prints: Hi, Ana +//│ > Hello, Ana +//│ > Hi, Ana +//│ > Hello, Ana! + +greet("Ana") + \ greeting: "Hi" + \ loud: true +//│ = "Hi, Ana!" + + diff --git a/hkmc2/shared/src/test/mlscript/flows/SelExpansion.mls b/hkmc2/shared/src/test/mlscript/flows/SelExpansion.mls index 297b022c62..80ac86fb4c 100644 --- a/hkmc2/shared/src/test/mlscript/flows/SelExpansion.mls +++ b/hkmc2/shared/src/test/mlscript/flows/SelExpansion.mls @@ -334,7 +334,8 @@ module CC with //│ flow:getX¹ <~ ((type CC) -> flow:x¹) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -:fixme // TODO: handle lifted class defs: the JS is missing the `.class` access to get the static member `CC2.class.getX` +// The companion path selects the class overload, including after lifting. +:expect 123 CC(123).getX //│ —————————————————| Flowed |————————————————————————————————————————————————————————————————————————— //│ CC⁴(123)‹app¹³›.CC⁴.getX²{ ~> CC⁴.getX²(CC⁴(123)‹app¹³›)‹app¹⁴› } @@ -342,7 +343,7 @@ CC(123).getX //│ app¹³ <~ CC //│ app¹³ ~> {getX: ⋅getX⁰} //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ ═══[RUNTIME ERROR] TypeError: CC2.getX is not a function +//│ = 123 :breakme // TODO: warn when this happens (accidental infinite recursion) @@ -432,7 +433,7 @@ let e = new Example.Test(123) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ e = Test(123) -:fixme // Same as test case above (missing `.class` access) +:expect Test(124) e.next //│ —————————————————| Flowed |————————————————————————————————————————————————————————————————————————— //│ e⁰.Example⁰.Test¹.next⁰{ ~> Example⁰.Test¹.next⁰(e⁰)‹app¹⁹› } @@ -440,6 +441,6 @@ e.next //│ e⁰ <~ Test(123) //│ e⁰ ~> {next: ⋅next⁰} //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ ═══[RUNTIME ERROR] TypeError: runtime.checkSelect(...).next is not a function +//│ = Test(124) diff --git a/hkmc2/shared/src/test/mlscript/flows/SelExpansionImport.mls b/hkmc2/shared/src/test/mlscript/flows/SelExpansionImport.mls index f290d38f29..de1c25e520 100644 --- a/hkmc2/shared/src/test/mlscript/flows/SelExpansionImport.mls +++ b/hkmc2/shared/src/test/mlscript/flows/SelExpansionImport.mls @@ -32,20 +32,16 @@ e.field //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ = 123 -// TODO: support -:e -:re +// Recover the imported companion module, including its class overload. +:expect Test(124) e.next -//│ ╔══[COMPILATION ERROR] Cannot access companion Test from the context of this selection -//│ ║ l.38: e.next -//│ ╙── ^^^^^^ //│ —————————————————| Flowed |————————————————————————————————————————————————————————————————————————— -//│ e⁰.nextˀˀˀ +//│ e⁰.Example⁰.Test⁰.next⁰{ ~> Example⁰.Test⁰.next⁰(e⁰)‹app⁰› } //│ where //│ e⁰ <~ Test(123) //│ e⁰ ~> {field: ⋅field⁰} {next: ⋅next⁰} //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. +//│ = Test(124) open Example @@ -64,20 +60,16 @@ let e = new Test(123) //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ e = Test(123) -// TODO: support -:e -:re +// Recover the imported companion module, including its class overload. +:expect Test(124) e.next -//│ ╔══[COMPILATION ERROR] Cannot access companion Test from the context of this selection -//│ ║ l.70: e.next -//│ ╙── ^^^^^^ //│ —————————————————| Flowed |————————————————————————————————————————————————————————————————————————— -//│ e¹.nextˀˀˀ +//│ e¹.Example⁰.Test⁰.next⁰{ ~> Example⁰.Test⁰.next⁰(e¹)‹app¹› } //│ where //│ e¹ <~ Test(123) //│ e¹ ~> {next: ⋅next¹} //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. +//│ = Test(124) @@ -85,13 +77,13 @@ e.next :e let e = Example.Test(123) //│ ╔══[COMPILATION ERROR] Cannot use non-term member Example in term position -//│ ║ l.86: let e = Example.Test(123) +//│ ║ l.78: let e = Example.Test(123) //│ ╙── ^^^^^^^ //│ —————————————————| Flowed |————————————————————————————————————————————————————————————————————————— //│ let e², -//│ e² = Example⁰.Test⁰(123)‹app⁰› +//│ e² = Example⁰.Test⁰(123)‹app²› //│ where -//│ app⁰ -> e² +//│ app² -> e² //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ e = Test(123) @@ -101,9 +93,9 @@ let e = Test(123) //│ ═══[COMPILATION ERROR] Cannot use non-term member Example in term position //│ —————————————————| Flowed |————————————————————————————————————————————————————————————————————————— //│ let e³, -//│ e³ = ⟨Example⁰.⟩Test⁰(123)‹app¹› +//│ e³ = ⟨Example⁰.⟩Test⁰(123)‹app³› //│ where -//│ app¹ -> e³ +//│ app³ -> e³ //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— //│ e = Test(123) @@ -111,7 +103,7 @@ let e = Test(123) :fixme // FIXME: error is probably due to some `bms.asTrm` weirdness (guess: due to lightweight elab?) Example.t //│ ╔══[COMPILATION ERROR] Cannot use non-term member Example in term position -//│ ║ l.112: Example.t +//│ ║ l.104: Example.t //│ ╙── ^^^^^^^ //│ —————————————————| Flowed |————————————————————————————————————————————————————————————————————————— //│ Example⁰.t⁰ @@ -126,10 +118,10 @@ Example.t :fixme // TODO: should get an inferred Type view of the field Example.t.field //│ ╔══[COMPILATION ERROR] Cannot use non-term member Example in term position -//│ ║ l.127: Example.t.field +//│ ║ l.119: Example.t.field //│ ╙── ^^^^^^^ //│ ╔══[COMPILATION ERROR] Unresolved selection: -//│ ║ l.127: Example.t.field +//│ ║ l.119: Example.t.field //│ ║ ^^^^^^^^^^^^^^^ //│ ╙── Type `type ⊤` does not contain member 'field' //│ —————————————————| Flowed |————————————————————————————————————————————————————————————————————————— @@ -142,10 +134,10 @@ Example.t.field :fixme // Same Example.t.next //│ ╔══[COMPILATION ERROR] Cannot use non-term member Example in term position -//│ ║ l.143: Example.t.next +//│ ║ l.135: Example.t.next //│ ╙── ^^^^^^^ //│ ╔══[COMPILATION ERROR] Unresolved selection: -//│ ║ l.143: Example.t.next +//│ ║ l.135: Example.t.next //│ ║ ^^^^^^^^^^^^^^ //│ ╙── Type `type ⊤` does not contain member 'next' //│ —————————————————| Flowed |————————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/handlers/Async.mls b/hkmc2/shared/src/test/mlscript/handlers/Async.mls index a7c4f348f4..93fccf7d28 100644 --- a/hkmc2/shared/src/test/mlscript/handlers/Async.mls +++ b/hkmc2/shared/src/test/mlscript/handlers/Async.mls @@ -112,18 +112,18 @@ fun g() = @async fun f1() = print("f1 started") - let work1 = new Promise[undefined]((resolve, reject) => + let work1 = new Promise[unsafe.undefined]((resolve, reject) => set work1resolve = resolve ) await(work1) print("f1 done") - work2resolve(undefined) + work2resolve(unsafe.undefined) @async fun f2() = print("f2 started") - let work2 = new Promise[undefined]((resolve, reject) => + let work2 = new Promise[unsafe.undefined]((resolve, reject) => set work2resolve = resolve - work1resolve(undefined) + work1resolve(unsafe.undefined) ) await(work2) print("f2 done") diff --git a/hkmc2/shared/src/test/mlscript/handlers/EffectsHygiene.mls b/hkmc2/shared/src/test/mlscript/handlers/EffectsHygiene.mls index 3b5685c0de..e4bbfca558 100644 --- a/hkmc2/shared/src/test/mlscript/handlers/EffectsHygiene.mls +++ b/hkmc2/shared/src/test/mlscript/handlers/EffectsHygiene.mls @@ -32,7 +32,7 @@ fun foo(h): Object = //│ let foo; //│ runtime.checkCall(runtime.resetEffects()); //│ foo = function foo(h) { -//│ runtime.checkArgs("foo", 1, true, arguments.length); +//│ runtime.checkArgs("foo", 1, arguments.length); //│ let A2, A3; //│ if (false === true) { //│ globalThis.Object.freeze(class A { diff --git a/hkmc2/shared/src/test/mlscript/handlers/EffectsInClasses.mls b/hkmc2/shared/src/test/mlscript/handlers/EffectsInClasses.mls index e301d975d2..e6af497698 100644 --- a/hkmc2/shared/src/test/mlscript/handlers/EffectsInClasses.mls +++ b/hkmc2/shared/src/test/mlscript/handlers/EffectsInClasses.mls @@ -103,7 +103,7 @@ let oops = oops.h //│ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'h') -let obj = mut {s: undefined} +let obj = mut {s: unsafe.undefined} //│ obj = {s: undefined} handle h = Effect with @@ -119,7 +119,7 @@ module A with f() () // defeats tail call optimization -let obj = new Object with +let obj = new with fun foo() = print("Hello") foo() //│ > Hello diff --git a/hkmc2/shared/src/test/mlscript/handlers/LeakingEffects.mls b/hkmc2/shared/src/test/mlscript/handlers/LeakingEffects.mls index 12ed0c1c12..969d17a23e 100644 --- a/hkmc2/shared/src/test/mlscript/handlers/LeakingEffects.mls +++ b/hkmc2/shared/src/test/mlscript/handlers/LeakingEffects.mls @@ -47,7 +47,7 @@ while //│ xs = [] // * Notes: -(mut []).pop() === undefined +(mut []).pop() === unsafe.undefined //│ = false (mut []).pop() === null //│ = false diff --git a/hkmc2/shared/src/test/mlscript/interop/Arrays.mls b/hkmc2/shared/src/test/mlscript/interop/Arrays.mls index 2014140b3b..65059f7f38 100644 --- a/hkmc2/shared/src/test/mlscript/interop/Arrays.mls +++ b/hkmc2/shared/src/test/mlscript/interop/Arrays.mls @@ -6,18 +6,10 @@ let arr = [true, false] //│ arr = [true, false] -// * Array methods annoyingly supply extra arguments, such as the index and the array itself -:e -:re +// * Array methods supply extra arguments, such as the index and the array itself, +// * which callbacks may ignore arr.map(=> _ === false) -//│ ╔══[COMPILATION ERROR] Resolution error in function type -//│ ║ l.261: fun map[U](f: (T, Int, Array[T]) -> U): Array[U] -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ -//│ ╟── Callback parameter list does not match its declared function type. -//│ ║ in Arrays.mls:12 -//│ ║ l.12: arr.map(=> _ === false) -//│ ╙── ^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function expected 1 argument but got 3 +//│ = [false, true] :sjs arr.map((e, ...) => e === false) @@ -41,25 +33,14 @@ arr.map((_, ...) => 1) :e arr.map((..., ...) => 1) -//│ ╔══[COMPILATION ERROR] Spread parameters must be the last in the parameter list. -//│ ║ l.43: arr.map((..., ...) => 1) -//│ ╙── ^^^ +//│ ╔══[COMPILATION ERROR] A parameter list cannot have more than one variadic parameter. +//│ ║ l.35: arr.map((..., ...) => 1) +//│ ╙── ^^^ //│ = [1, 1] -:e -:re +:expect [[true, false], [true, false]] arr.map((..., e) => e) -//│ ╔══[COMPILATION ERROR] Spread parameters must be the last in the parameter list. -//│ ║ l.51: arr.map((..., e) => e) -//│ ╙── ^^^ -//│ ╔══[COMPILATION ERROR] Resolution error in function type -//│ ║ l.261: fun map[U](f: (T, Int, Array[T]) -> U): Array[U] -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ -//│ ╟── Callback parameter list does not match its declared function type. -//│ ║ in Arrays.mls:51 -//│ ║ l.51: arr.map((..., e) => e) -//│ ╙── ^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function expected 2 arguments but got 3 +//│ = [[true, false], [true, false]] arr.map(pass1(=> _ === false)) //│ = [false, true] @@ -82,22 +63,22 @@ Array(1) :e Array() //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.83: Array() +//│ ║ l.64: Array() //│ ║ ^^^^^^^ //│ ╟── Constructor of class 'Array' expected 1 argument, but got 0 //│ ║ in ../decls/Prelude.mls -//│ ║ l.241: declare class Array[T](val length: Int) { +//│ ║ l.261: declare class Array[T](val length: Int) { //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ = [] :e Array(1, 2, 3) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.94: Array(1, 2, 3) +//│ ║ l.75: Array(1, 2, 3) //│ ║ ^^^^^^^^^^^^^^ //│ ╟── Constructor of class 'Array' expected 1 argument, but got 3 //│ ║ in ../decls/Prelude.mls -//│ ║ l.241: declare class Array[T](val length: Int) { +//│ ║ l.261: declare class Array[T](val length: Int) { //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ = [1, 2, 3] diff --git a/hkmc2/shared/src/test/mlscript/interop/Null.mls b/hkmc2/shared/src/test/mlscript/interop/Null.mls index e0d6f73216..b4ccaf0dde 100644 --- a/hkmc2/shared/src/test/mlscript/interop/Null.mls +++ b/hkmc2/shared/src/test/mlscript/interop/Null.mls @@ -2,10 +2,10 @@ :js -null == undefined +null == unsafe.undefined //│ = false -null === undefined +null === unsafe.undefined //│ = false null === null @@ -14,7 +14,7 @@ null === null console.log(null is null) //│ > true -Object.create(null).("toString") is undefined +Object.create(null).("toString") is unsafe.undefined //│ = true diff --git a/hkmc2/shared/src/test/mlscript/interop/Objects.mls b/hkmc2/shared/src/test/mlscript/interop/Objects.mls index 9e9438fce1..eabb5b0811 100644 --- a/hkmc2/shared/src/test/mlscript/interop/Objects.mls +++ b/hkmc2/shared/src/test/mlscript/interop/Objects.mls @@ -2,7 +2,7 @@ :js -let rcd = { x: 0, y: undefined, z: null} +let rcd = { x: 0, y: unsafe.undefined, z: null} //│ rcd = {x: 0, y: undefined, z: null} rcd!constructor diff --git a/hkmc2/shared/src/test/mlscript/invalml/InvalMLBasicADTs.mls b/hkmc2/shared/src/test/mlscript/invalml/InvalMLBasicADTs.mls index 55c9a3e61b..024a96d889 100644 --- a/hkmc2/shared/src/test/mlscript/invalml/InvalMLBasicADTs.mls +++ b/hkmc2/shared/src/test/mlscript/invalml/InvalMLBasicADTs.mls @@ -164,10 +164,8 @@ fun badSum(xs) = if xs is Cons(x, xs) then x + badSum(xs) //│ ╔══[COMPILATION ERROR] Expect 2 cases, but 1 got. -//│ ║ l.164: if xs is -//│ ║ ^^^^^ //│ ║ l.165: Cons(x, xs) then x + badSum(xs) -//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ :e fun badSum(xs) = @@ -175,18 +173,14 @@ fun badSum(xs) = Cons(x, xs) then x + badSum(xs) Cons(x, xs) then x + badSum(xs) //│ ╔══[COMPILATION ERROR] Duplicate match branches. -//│ ║ l.174: if xs is -//│ ║ ^^^^^ -//│ ║ l.175: Cons(x, xs) then x + badSum(xs) -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -//│ ║ l.176: Cons(x, xs) then x + badSum(xs) +//│ ║ l.173: Cons(x, xs) then x + badSum(xs) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.174: Cons(x, xs) then x + badSum(xs) //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Expect 2 cases, but 1 got. -//│ ║ l.174: if xs is -//│ ║ ^^^^^ -//│ ║ l.175: Cons(x, xs) then x + badSum(xs) -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -//│ ║ l.176: Cons(x, xs) then x + badSum(xs) +//│ ║ l.173: Cons(x, xs) then x + badSum(xs) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.174: Cons(x, xs) then x + badSum(xs) //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -294,14 +288,8 @@ fun foo(x) = None then 0 42 then 42 //│ ╔══[COMPILATION ERROR] Mixing ADT pattern matching and general matching is not supported yet. -//│ ║ l.292: if x is -//│ ║ ^^^^ -//│ ║ l.293: Some(x) then 1 -//│ ║ ^^^^^^^^^^^^^^^^^^ -//│ ║ l.294: None then 0 -//│ ║ ^^^^^^^^^^^^^^^ -//│ ║ l.295: 42 then 42 -//│ ╙── ^^^^^^^^^^^^^^ +//│ ║ l.289: 42 then 42 +//│ ╙── ^^^^^^^^^^ //│ ═══[COMPILATION ERROR] Variable not found: arg$Some$0$ diff --git a/hkmc2/shared/src/test/mlscript/invalml/InvalMLFuns.mls b/hkmc2/shared/src/test/mlscript/invalml/InvalMLFuns.mls index 633c026863..ff0e5b9c6c 100644 --- a/hkmc2/shared/src/test/mlscript/invalml/InvalMLFuns.mls +++ b/hkmc2/shared/src/test/mlscript/invalml/InvalMLFuns.mls @@ -66,10 +66,10 @@ foo :e fun foo[outer, R <: outer](r1: Region[R]) = region r2 in 42 -//│ ╔══[COMPILATION ERROR] Unsupported type parameter outer binding +//│ ╔══[COMPILATION ERROR] Unsupported type parameter shape: outer binding //│ ║ l.67: fun foo[outer, R <: outer](r1: Region[R]) = //│ ╙── ^^^^^ -//│ ╔══[COMPILATION ERROR] Unsupported type parameter operator application +//│ ╔══[COMPILATION ERROR] Unsupported type parameter shape: operator application //│ ║ l.67: fun foo[outer, R <: outer](r1: Region[R]) = //│ ╙── ^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Name not found: R diff --git a/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls b/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls index 73aa842ea0..5f2b7413b1 100644 --- a/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls +++ b/hkmc2/shared/src/test/mlscript/invalml/InvalMLPrelude.mls @@ -47,6 +47,9 @@ declare module source with name: Str file: Str +declare module unsafe with + val id"undefined" + declare class Array[T](val length: Int): Array[T] declare class Symbol @@ -170,6 +173,8 @@ declare module debug with declare module annotations with object pure + object fresh + fun jsSymbol(path: Any) object untyped object tailrec object tailcall diff --git a/hkmc2/shared/src/test/mlscript/invalml/InvalMLUsefulExtrusion.mls b/hkmc2/shared/src/test/mlscript/invalml/InvalMLUsefulExtrusion.mls index 3cfd77fc65..da8bb2a927 100644 --- a/hkmc2/shared/src/test/mlscript/invalml/InvalMLUsefulExtrusion.mls +++ b/hkmc2/shared/src/test/mlscript/invalml/InvalMLUsefulExtrusion.mls @@ -164,10 +164,10 @@ fun foo[outer, R <: outer](r1: Region[R]) = freeze(r2, () => wield(r1) ) -//│ ╔══[COMPILATION ERROR] Unsupported type parameter outer binding +//│ ╔══[COMPILATION ERROR] Unsupported type parameter shape: outer binding //│ ║ l.162: fun foo[outer, R <: outer](r1: Region[R]) = //│ ╙── ^^^^^ -//│ ╔══[COMPILATION ERROR] Unsupported type parameter operator application +//│ ╔══[COMPILATION ERROR] Unsupported type parameter shape: operator application //│ ║ l.162: fun foo[outer, R <: outer](r1: Region[R]) = //│ ╙── ^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Name not found: R diff --git a/hkmc2/shared/src/test/mlscript/lifter/AnonClasses.mls b/hkmc2/shared/src/test/mlscript/lifter/AnonClasses.mls index 2a31e61ec9..df763d8ff7 100644 --- a/hkmc2/shared/src/test/mlscript/lifter/AnonClasses.mls +++ b/hkmc2/shared/src/test/mlscript/lifter/AnonClasses.mls @@ -2,7 +2,7 @@ fun foo() = - new Object with + new Object() with fun test = 1 foo().test diff --git a/hkmc2/shared/src/test/mlscript/lifter/Mutation.mls b/hkmc2/shared/src/test/mlscript/lifter/Mutation.mls index 170cae4b55..c68123601b 100644 --- a/hkmc2/shared/src/test/mlscript/lifter/Mutation.mls +++ b/hkmc2/shared/src/test/mlscript/lifter/Mutation.mls @@ -76,7 +76,7 @@ b_x.1() fun foo() = - let x = undefined + let x = unsafe.undefined fun bar() = x = 2 [bar, () => x] diff --git a/hkmc2/shared/src/test/mlscript/lifter/PrivateMutableFields.mls b/hkmc2/shared/src/test/mlscript/lifter/PrivateMutableFields.mls index 341f320d88..7d13e25bc0 100644 --- a/hkmc2/shared/src/test/mlscript/lifter/PrivateMutableFields.mls +++ b/hkmc2/shared/src/test/mlscript/lifter/PrivateMutableFields.mls @@ -42,7 +42,7 @@ class Foo(x) with //│ private val y⁰; //│ private val state⁰; //│ constructor(Foo, y) { -//│ do super⁰(); +//│ do super⁰(undefined); //│ end; //│ set $anon⁰.this.Foo⁰ = Foo; //│ set $anon⁰.this.y⁰ = y; @@ -102,7 +102,7 @@ class AccessorBox with //│ $anon5 = this //│ } //│ constructor(AccessorBox2) { -//│ super(); +//│ super(undefined); //│ this.#AccessorBox = AccessorBox2; //│ } //│ #AccessorBox; diff --git a/hkmc2/shared/src/test/mlscript/meta/CompilerFiction.mls b/hkmc2/shared/src/test/mlscript/meta/CompilerFiction.mls index ec15ce0ecb..3b91c7ef2f 100644 --- a/hkmc2/shared/src/test/mlscript/meta/CompilerFiction.mls +++ b/hkmc2/shared/src/test/mlscript/meta/CompilerFiction.mls @@ -25,7 +25,7 @@ js.oops //│ ║ ^^^^^^^ //│ ╟── Module 'js' does not contain member 'oops' //│ ║ in ../decls/Prelude.mls -//│ ║ l.438: declare module js { +//│ ║ l.468: declare module js { //│ ╙── ^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. diff --git a/hkmc2/shared/src/test/mlscript/newres/Annotations.mls b/hkmc2/shared/src/test/mlscript/newres/Annotations.mls index b334a084c4..cc4517e9a4 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Annotations.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Annotations.mls @@ -80,17 +80,16 @@ fun customAnnotation(arg) = //│ ║ l.76: @Custom(arg) //│ ╙── ^^^^^^^^^^^^ -// A qualified annotation cannot wait for its receiver's forward definition. -:e +// A declared module's member identity is available before its body completes. +:w fun forwardQualified() = @Later.noInline fun inner(x) = x inner(5) module Later { object noInline } -//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ╔══[WARNING] This annotation has no effect. //│ ║ l.86: @Later.noInline -//│ ║ ^^^^^^^^^^^^^^^ -//│ ╙── An annotation's main symbol must be uniquely known when the annotation is elaborated. +//│ ╙── ^^^^^^^^^^^^^^^ // Nor can an annotation wait for a forward wildcard source or an alias's value. :e @@ -101,7 +100,7 @@ fun forwardWildcard() = inner(6) module Future { object noInline } //│ ╔══[COMPILATION ERROR] Resolution error in wildcard-open reference -//│ ║ l.99: @noInline +//│ ║ l.98: @noInline //│ ║ ^^^^^^^^^ //│ ╙── An annotation's main symbol must be uniquely known when the annotation is elaborated. @@ -112,7 +111,7 @@ fun forwardAlias() = inner(7) val alias = Later //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.110: @alias.noInline +//│ ║ l.109: @alias.noInline //│ ║ ^^^^^^^^^^^^^^^ //│ ╙── An annotation's main symbol must be uniquely known when the annotation is elaborated. //│ alias = class Later { noInline: noInline } @@ -128,7 +127,7 @@ fun ambiguousAnnotation() = fun inner(x) = x inner(8) //│ ╔══[COMPILATION ERROR] Resolution error in wildcard-open reference -//│ ║ l.127: @marker +//│ ║ l.126: @marker //│ ║ ^^^^^^^ //│ ╙── An annotation's main symbol must be uniquely known when the annotation is elaborated. @@ -142,6 +141,6 @@ fun lateAmbiguity() = inner(9) module Last { object noInline } //│ ╔══[COMPILATION ERROR] Resolution error in wildcard-open reference -//│ ║ l.140: @noInline +//│ ║ l.139: @noInline //│ ║ ^^^^^^^^^ //│ ╙── An annotation's main symbol must be uniquely known when the annotation is elaborated. diff --git a/hkmc2/shared/src/test/mlscript/newres/Arrays.mls b/hkmc2/shared/src/test/mlscript/newres/Arrays.mls index 88a9927cec..e1aa8a7e61 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Arrays.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Arrays.mls @@ -82,17 +82,10 @@ values([Item(4)]).map((item, ...) => item.value) [10, 20].map((item, ...rest) => rest.0 + rest.1.length + 1) //│ = [3, 4] -// Fixed callbacks must accept all three arguments supplied by Array.map. -:ge +// Callbacks may drop the index and array arguments supplied by Array.map. +:expect [1] [1].map(x => x) -//│ ╔══[COMPILATION ERROR] Resolution error in function type -//│ ║ l.261: fun map[U](f: (T, Int, Array[T]) -> U): Array[U] -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ -//│ ╟── Callback parameter list does not match its declared function type. -//│ ║ in Arrays.mls:87 -//│ ║ l.87: [1].map(x => x) -//│ ╙── ^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function expected 1 argument but got 3 +//│ = [1] // Mutable arrays do not propagate stale initializer element shapes to callbacks. :ge @@ -102,11 +95,11 @@ let xs = mut [Original(1)] set xs.0 = Replacement(2) xs.map((x, ...) => x.original) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.103: xs.map((x, ...) => x.original) -//│ ║ ^^^^^^^^^^ +//│ ║ l.96: xs.map((x, ...) => x.original) +//│ ║ ^^^^^^^^^^ //│ ╟── Instance of class 'Replacement' does not contain member 'original' -//│ ║ l.102: set xs.0 = Replacement(2) -//│ ╙── ^^^^^^^^^^^^^^ +//│ ║ l.95: set xs.0 = Replacement(2) +//│ ╙── ^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ xs = [Replacement(2)] @@ -137,22 +130,22 @@ new String("hello") is String :ge new Array[Int](1, 2) //│ ╔══[COMPILATION ERROR] Resolution error in object instantiation -//│ ║ l.138: new Array[Int](1, 2) +//│ ║ l.131: new Array[Int](1, 2) //│ ║ ^^^^^^^^^^^^^^^^^^^^ //│ ╟── Constructor of class 'Array' expected 1 argument, but got 2 //│ ║ in ../decls/Prelude.mls -//│ ║ l.241: declare class Array[T](val length: Int) { +//│ ║ l.261: declare class Array[T](val length: Int) { //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ = [1, 2] :ge new Array[Int]() //│ ╔══[COMPILATION ERROR] Resolution error in object instantiation -//│ ║ l.149: new Array[Int]() +//│ ║ l.142: new Array[Int]() //│ ║ ^^^^^^^^^^^^^^^^ //│ ╟── Constructor of class 'Array' expected 1 argument, but got 0 //│ ║ in ../decls/Prelude.mls -//│ ║ l.241: declare class Array[T](val length: Int) { +//│ ║ l.261: declare class Array[T](val length: Int) { //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ = [] @@ -199,10 +192,10 @@ class Item(val value: Int) class Total(val total: Int) [Item(1)].reduce((acc, item, ...) => acc.missing, Total(0)) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.200: [Item(1)].reduce((acc, item, ...) => acc.missing, Total(0)) +//│ ║ l.193: [Item(1)].reduce((acc, item, ...) => acc.missing, Total(0)) //│ ║ ^^^^^^^^^^^ //│ ╟── Instance of class 'Total' does not contain member 'missing' -//│ ║ l.200: [Item(1)].reduce((acc, item, ...) => acc.missing, Total(0)) +//│ ║ l.193: [Item(1)].reduce((acc, item, ...) => acc.missing, Total(0)) //│ ╙── ^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -211,12 +204,12 @@ class Total(val total: Int) :e [1, 2].reduce((acc, x, ...) => acc + x) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.212: [1, 2].reduce((acc, x, ...) => acc + x) +//│ ║ l.205: [1, 2].reduce((acc, x, ...) => acc + x) //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Function 'reduce' expected 2 arguments, but got 1 //│ ║ in ../decls/Prelude.mls -//│ ║ l.275: fun reduce[U](f: (U, T, Int, Array[T]) -> U, initial: U): U -//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.296: fun reduce[U](f: (U, T, drop Int, drop Array[T]) -> U, initial: U): U +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ = 3 // A tuple created in one activation of `mk` and passed to another has the path @@ -251,10 +244,10 @@ let items = mut [Item(4)] class Item(val value: Int) (new Item(1)).[0] //│ ╔══[COMPILATION ERROR] Resolution error in array index -//│ ║ l.252: (new Item(1)).[0] +//│ ║ l.245: (new Item(1)).[0] //│ ║ ^^^^^^^^^^^^^^^ //│ ╟── Instance of class 'Item' does not support checked array indexing. -//│ ║ l.252: (new Item(1)).[0] +//│ ║ l.245: (new Item(1)).[0] //│ ║ ^^^^^^^^^^^ //│ ╙── Use '![...]' for a dynamic access. //│ ═══[RUNTIME ERROR] TypeError: tmp49.at is not a function @@ -263,8 +256,8 @@ class Item(val value: Int) :e fun first(xs: Array[Int]) = [xs![0].whatever, xs.[0].whatever] //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.264: fun first(xs: Array[Int]) = [xs![0].whatever, xs.[0].whatever] +//│ ║ l.257: fun first(xs: Array[Int]) = [xs![0].whatever, xs.[0].whatever] //│ ║ ^^^^^^^^^^^^^ //│ ╟── Value of type 'Int' does not contain member 'whatever' -//│ ║ l.264: fun first(xs: Array[Int]) = [xs![0].whatever, xs.[0].whatever] +//│ ║ l.257: fun first(xs: Array[Int]) = [xs![0].whatever, xs.[0].whatever] //│ ╙── ^^^ diff --git a/hkmc2/shared/src/test/mlscript/newres/BadApps.mls b/hkmc2/shared/src/test/mlscript/newres/BadApps.mls index 735c557d46..f0404f63fa 100644 --- a/hkmc2/shared/src/test/mlscript/newres/BadApps.mls +++ b/hkmc2/shared/src/test/mlscript/newres/BadApps.mls @@ -48,7 +48,14 @@ call(M) //│ ║ ^^^ //│ ╟── Module 'M' cannot be called like a function. //│ ║ l.43: module M -//│ ╙── ^^^^^^^^ +//│ ║ ^^^^^^^^ +//│ ╟── Value of unknown shape cannot be called like a function. +//│ ╟── Parameter 'x' admits values of unknown shape. +//│ ║ l.44: fun call(x) = x() +//│ ║ ^ +//│ ╟── 'call' is exposed by this compilation unit. +//│ ║ l.44: fun call(x) = x() +//│ ╙── ^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. // Plain class values require new, including when passed through a parameter. @@ -57,11 +64,18 @@ class Plain fun instantiate(cls) = cls() instantiate(Plain) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.57: fun instantiate(cls) = cls() +//│ ║ l.64: fun instantiate(cls) = cls() //│ ║ ^^^^^ //│ ╟── Class 'Plain' must be instantiated with 'new'. -//│ ║ l.56: class Plain -//│ ╙── ^^^^^^^^^^^ +//│ ║ l.63: class Plain +//│ ║ ^^^^^^^^^^^ +//│ ╟── Value of unknown shape cannot be called like a function. +//│ ╟── Parameter 'cls' admits values of unknown shape. +//│ ║ l.64: fun instantiate(cls) = cls() +//│ ║ ^^^ +//│ ╟── 'instantiate' is exposed by this compilation unit. +//│ ║ l.64: fun instantiate(cls) = cls() +//│ ╙── ^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. // A fully applied constructor really has received all its argument lists. @@ -70,11 +84,18 @@ class Curried(x)(y) fun overapply(instance) = instance() overapply(Curried(1)(2)) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.70: fun overapply(instance) = instance() +//│ ║ l.84: fun overapply(instance) = instance() //│ ║ ^^^^^^^^^^ //│ ╟── Instance of class 'Curried' cannot receive more argument lists. -//│ ║ l.71: overapply(Curried(1)(2)) -//│ ╙── ^^^^^^^^^^^^^ +//│ ║ l.85: overapply(Curried(1)(2)) +//│ ║ ^^^^^^^^^^^^^ +//│ ╟── Value of unknown shape cannot be called like a function. +//│ ╟── Parameter 'instance' admits values of unknown shape. +//│ ║ l.84: fun overapply(instance) = instance() +//│ ║ ^^^^^^^^ +//│ ╟── 'overapply' is exposed by this compilation unit. +//│ ║ l.84: fun overapply(instance) = instance() +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. // Explicitly constructed instances must not be told to add another new. @@ -83,10 +104,17 @@ class Plain fun overapply(instance) = instance() overapply(new Plain) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.83: fun overapply(instance) = instance() -//│ ║ ^^^^^^^^^^ +//│ ║ l.104: fun overapply(instance) = instance() +//│ ║ ^^^^^^^^^^ //│ ╟── Instance of class 'Plain' cannot receive more argument lists. -//│ ║ l.84: overapply(new Plain) -//│ ╙── ^^^^^^^^^ +//│ ║ l.105: overapply(new Plain) +//│ ║ ^^^^^^^^^ +//│ ╟── Value of unknown shape cannot be called like a function. +//│ ╟── Parameter 'instance' admits values of unknown shape. +//│ ║ l.104: fun overapply(instance) = instance() +//│ ║ ^^^^^^^^ +//│ ╟── 'overapply' is exposed by this compilation unit. +//│ ║ l.104: fun overapply(instance) = instance() +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. diff --git a/hkmc2/shared/src/test/mlscript/newres/Basics.mls b/hkmc2/shared/src/test/mlscript/newres/Basics.mls index e4cf0a8a12..372f4bcefc 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Basics.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Basics.mls @@ -50,10 +50,10 @@ Predef.id //│ | | .exit MarkedShape (member:id¹, term:Predef¹/id⁰, ⋅id⁰) //│ | | = MarkedShape(member:id¹, ↗⟨term:Predef¹/id⁰⟩%⟨⋅id⁰⟩ϵ) //│ | | PUSH MarkedShape(member:id¹, ↗⟨term:Predef¹/id⁰⟩%⟨⋅id⁰⟩ϵ) -//│ | ~> member:Predef⁰.id -//│ ~> { member:Predef⁰.id } -//│ listenTerm: trm = member:Predef⁰.id -//│ listen: trm = member:Predef⁰.id +//│ | ~> member:Predef⁰.id‹term:Predef¹/id⁰› +//│ ~> { member:Predef⁰.id‹term:Predef¹/id⁰› } +//│ listenTerm: trm = member:Predef⁰.id‹term:Predef¹/id⁰› +//│ listen: trm = member:Predef⁰.id‹term:Predef¹/id⁰› //│ listenBMS: bms = function //│ listenedBMS: bms = function //│ fromBMS: bms = member:id¹, sh = member:id¹, flow = ⋅id⁰, markss = List() diff --git a/hkmc2/shared/src/test/mlscript/newres/BuiltinFunction.mls b/hkmc2/shared/src/test/mlscript/newres/BuiltinFunction.mls index 2524a80aea..5ffaac2f7f 100644 --- a/hkmc2/shared/src/test/mlscript/newres/BuiltinFunction.mls +++ b/hkmc2/shared/src/test/mlscript/newres/BuiltinFunction.mls @@ -126,7 +126,7 @@ test((x) => x.value) //│ ║ l.117: fun test(f: Function) = f(1, 2, 3) //│ ║ ^^^^^^^^ //│ ╙── The 'Function' type does not specify parameter types. -//│ ═══[RUNTIME ERROR] Error: Function expected 1 argument but got 3 +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. // Erasure checks callback inputs even when a private consumer never calls it. :ge diff --git a/hkmc2/shared/src/test/mlscript/newres/Builtins.mls b/hkmc2/shared/src/test/mlscript/newres/Builtins.mls index 97a49a86e7..f82738f65b 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Builtins.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Builtins.mls @@ -3,11 +3,34 @@ // :de :sr +:w new Map +//│ ╔══[WARNING] This 'new' expression partially applies class 'Map' instead of constructing an instance. +//│ ║ l.7: new Map +//│ ║ ^^^^^^^ +//│ ╟── Use `new Map()` to construct an instance, or `=> new Map()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ in ../decls/Prelude.mls +//│ ║ l.97: constructor(entries?: Iterable[[K, V]] | null) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ————————————————| Resolved |———————————————————————————————————————————————————————————————————————— //│ new Map⁰ //│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— -//│ = Map(0) {} +//│ = fun + +// A missing argument list leaves the declared constructor partially applied. +:expect 0 +:w +(new Map)([]).size +//│ ╔══[WARNING] This 'new' expression partially applies class 'Map' instead of constructing an instance. +//│ ║ l.24: (new Map)([]).size +//│ ║ ^^^^^^^ +//│ ╟── Use `new Map()` to construct an instance, or `=> new Map()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ in ../decls/Prelude.mls +//│ ║ l.97: constructor(entries?: Iterable[[K, V]] | null) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ = 0 // :de :sr diff --git a/hkmc2/shared/src/test/mlscript/newres/ConstructorInstances.mls b/hkmc2/shared/src/test/mlscript/newres/ConstructorInstances.mls index 0de1443fb4..9ad5003aa3 100644 --- a/hkmc2/shared/src/test/mlscript/newres/ConstructorInstances.mls +++ b/hkmc2/shared/src/test/mlscript/newres/ConstructorInstances.mls @@ -217,11 +217,11 @@ let second = new Box(Second(22)) [first.copy().item.first, second.copy().item.second] //│ ╔══[COMPILATION ERROR] Resolution error in selection //│ ║ l.217: [first.copy().item.first, second.copy().item.second] -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^ -//│ ╟── Instance of class 'First' does not contain member 'second' -//│ ║ l.215: let first = new Box(First(21)) -//│ ║ ^^^^^^^^^ -//│ ╟── Cannot resolve member 'second' of a value with unknown shape. +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Instance of class 'Second' does not contain member 'first' +//│ ║ l.216: let second = new Box(Second(22)) +//│ ║ ^^^^^^^^^^ +//│ ╟── Cannot resolve member 'first' of a value with unknown shape. //│ ╟── Parameter 'item' admits values of unknown shape. //│ ║ l.213: class Box(val item) with //│ ║ ^^^^^^^^ @@ -230,11 +230,11 @@ let second = new Box(Second(22)) //│ ╙── ^^^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Resolution error in selection //│ ║ l.217: [first.copy().item.first, second.copy().item.second] -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ -//│ ╟── Instance of class 'Second' does not contain member 'first' -//│ ║ l.216: let second = new Box(Second(22)) -//│ ║ ^^^^^^^^^^ -//│ ╟── Cannot resolve member 'first' of a value with unknown shape. +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Instance of class 'First' does not contain member 'second' +//│ ║ l.215: let first = new Box(First(21)) +//│ ║ ^^^^^^^^^ +//│ ╟── Cannot resolve member 'second' of a value with unknown shape. //│ ╟── Parameter 'item' admits values of unknown shape. //│ ║ l.213: class Box(val item) with //│ ║ ^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/newres/DeclarationLocations.mls b/hkmc2/shared/src/test/mlscript/newres/DeclarationLocations.mls index 6a473f2bcd..c777bc68fb 100644 --- a/hkmc2/shared/src/test/mlscript/newres/DeclarationLocations.mls +++ b/hkmc2/shared/src/test/mlscript/newres/DeclarationLocations.mls @@ -277,7 +277,7 @@ twice(x => x + 1) //│ ╟── Function 'twice' expected 2 arguments, but got 1 //│ ║ l.270: fun twice(f: Int -> Int, x: Int): Int = //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'twice' expected 2 arguments but got 1 +//│ ═══[RUNTIME ERROR] Error: Function 'twice' expected at least 2 arguments but got 1 // So do those citing a member, such as an exposed function with an unknown parameter. :e @@ -288,8 +288,6 @@ fun exposed(f) = //│ ║ l.285: let y = f(0) //│ ║ ^^^^ //│ ╟── Value of unknown shape cannot be called like a function. -//│ ║ l.284: fun exposed(f) = -//│ ║ ^ //│ ╟── Parameter 'f' admits values of unknown shape. //│ ║ l.284: fun exposed(f) = //│ ║ ^ @@ -302,10 +300,10 @@ fun exposed(f) = declare fun declared(x: Int): Int declared(1, 2) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.303: declared(1, 2) +//│ ║ l.301: declared(1, 2) //│ ║ ^^^^^^^^^^^^^^ //│ ╟── Function 'declared' expected 1 argument, but got 2 -//│ ║ l.302: declare fun declared(x: Int): Int +//│ ║ l.300: declare fun declared(x: Int): Int //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] TypeError: globalThis.declared is not a function @@ -317,9 +315,36 @@ class Opened with x + 2 new Opened().f(1, 2) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.318: new Opened().f(1, 2) +//│ ║ l.316: new Opened().f(1, 2) //│ ║ ^^^^^^^^^^^^^^^^^^^^ //│ ╟── Function 'f' expected 1 argument, but got 2 -//│ ║ l.315: open fun f(x) = +//│ ║ l.313: open fun f(x) = //│ ╙── ^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'f' expected 1 argument but got 2 +//│ = 3 + +// Each call cites the unknown parameter's declaration without repeating the callee span. +:e +fun invokeAtEachUse(f) = + let alias = f + f() + alias() +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.330: alias() +//│ ║ ^^^^^^^ +//│ ╟── Value of unknown shape cannot be called like a function. +//│ ╟── Parameter 'f' admits values of unknown shape. +//│ ║ l.327: fun invokeAtEachUse(f) = +//│ ║ ^ +//│ ╟── 'invokeAtEachUse' is exposed by this compilation unit. +//│ ║ l.327: fun invokeAtEachUse(f) = +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.329: f() +//│ ║ ^^^ +//│ ╟── Value of unknown shape cannot be called like a function. +//│ ╟── Parameter 'f' admits values of unknown shape. +//│ ║ l.327: fun invokeAtEachUse(f) = +//│ ║ ^ +//│ ╟── 'invokeAtEachUse' is exposed by this compilation unit. +//│ ║ l.327: fun invokeAtEachUse(f) = +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/newres/DeclaredTypes.mls b/hkmc2/shared/src/test/mlscript/newres/DeclaredTypes.mls index b53381168d..5ce803b141 100644 --- a/hkmc2/shared/src/test/mlscript/newres/DeclaredTypes.mls +++ b/hkmc2/shared/src/test/mlscript/newres/DeclaredTypes.mls @@ -60,17 +60,17 @@ val box: Base = new Child box.extra (new Child as Base).extra //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.61: (new Child as Base).extra -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^ -//│ ╟── Value of type 'Base' does not contain member 'extra' -//│ ║ l.61: (new Child as Base).extra -//│ ╙── ^^^^ -//│ ╔══[COMPILATION ERROR] Resolution error in selection //│ ║ l.60: box.extra //│ ║ ^^^^^^^^^ //│ ╟── Value of type 'Base' does not contain member 'extra' //│ ║ l.59: val box: Base = new Child //│ ╙── ^^^^ +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.61: (new Child as Base).extra +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'extra' +//│ ║ l.61: (new Child as Base).extra +//│ ╙── ^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ box = Child { extra: 42 } @@ -565,8 +565,6 @@ invoke(Box(() => 42)) //│ ║ l.562: fun invoke(box: Box) = box.callback() //│ ║ ^^^^^^^^^^^^^^ //│ ╟── Value of unknown shape cannot be called like a function. -//│ ║ l.562: fun invoke(box: Box) = box.callback() -//│ ║ ^^^^^^^^^^^^ //│ ╟── Constructor parameter 'callback' has no type annotation. //│ ║ l.561: class Box(val callback) //│ ║ ^^^^^^^^^^^^ @@ -581,13 +579,13 @@ invoke(Box(() => 42)) class Box[X](val value: [X, X]) fun foo[A](x: Box[A]) = x.value.0.oops //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.582: fun foo[A](x: Box[A]) = x.value.0.oops +//│ ║ l.580: fun foo[A](x: Box[A]) = x.value.0.oops //│ ║ ^^^^^^^^^^^^^^ //│ ╟── Cannot resolve member 'oops' of a value with unknown shape. -//│ ║ l.582: fun foo[A](x: Box[A]) = x.value.0.oops +//│ ║ l.580: fun foo[A](x: Box[A]) = x.value.0.oops //│ ║ ^ //│ ╟── Type parameter 'A' does not specify a member interface. -//│ ║ l.582: fun foo[A](x: Box[A]) = x.value.0.oops +//│ ║ l.580: fun foo[A](x: Box[A]) = x.value.0.oops //│ ╙── ^ // Concrete callers cannot hide the annotation responsible for an abstract tuple element. @@ -598,13 +596,13 @@ class Box[X](val value: [X, X]) private fun foo[A](x: Box[A]) = x.value.0.oops foo[Item](Box[Item]([new Item, new Item])) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.598: private fun foo[A](x: Box[A]) = x.value.0.oops +//│ ║ l.596: private fun foo[A](x: Box[A]) = x.value.0.oops //│ ║ ^^^^^^^^^^^^^^ //│ ╟── Cannot resolve member 'oops' of a value with unknown shape. -//│ ║ l.598: private fun foo[A](x: Box[A]) = x.value.0.oops +//│ ║ l.596: private fun foo[A](x: Box[A]) = x.value.0.oops //│ ║ ^ //│ ╟── Type parameter 'A' does not specify a member interface. -//│ ║ l.598: private fun foo[A](x: Box[A]) = x.value.0.oops +//│ ║ l.596: private fun foo[A](x: Box[A]) = x.value.0.oops //│ ╙── ^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -614,26 +612,26 @@ class Box[X](val value: [X, X]) type Wrapped[T] = Box[T] fun foo[A](factory: () -> Wrapped[A]) = factory().value.1.oops //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.615: fun foo[A](factory: () -> Wrapped[A]) = factory().value.1.oops +//│ ║ l.613: fun foo[A](factory: () -> Wrapped[A]) = factory().value.1.oops //│ ║ ^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Cannot resolve member 'oops' of a value with unknown shape. -//│ ║ l.615: fun foo[A](factory: () -> Wrapped[A]) = factory().value.1.oops +//│ ║ l.613: fun foo[A](factory: () -> Wrapped[A]) = factory().value.1.oops //│ ║ ^ //│ ╟── Type parameter 'A' does not specify a member interface. -//│ ║ l.615: fun foo[A](factory: () -> Wrapped[A]) = factory().value.1.oops +//│ ║ l.613: fun foo[A](factory: () -> Wrapped[A]) = factory().value.1.oops //│ ╙── ^ // Tuple annotations do not need a nominal receiver to produce an abstract element. :ge fun foo[A](x: [A, A]) = x.0.oops //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.628: fun foo[A](x: [A, A]) = x.0.oops +//│ ║ l.626: fun foo[A](x: [A, A]) = x.0.oops //│ ║ ^^^^^^^^ //│ ╟── Cannot resolve member 'oops' of a value with unknown shape. -//│ ║ l.628: fun foo[A](x: [A, A]) = x.0.oops +//│ ║ l.626: fun foo[A](x: [A, A]) = x.0.oops //│ ║ ^ //│ ╟── Type parameter 'A' does not specify a member interface. -//│ ║ l.628: fun foo[A](x: [A, A]) = x.0.oops +//│ ║ l.626: fun foo[A](x: [A, A]) = x.0.oops //│ ╙── ^ // An unannotated field can be reached through a function result annotation and an alias. @@ -642,14 +640,14 @@ class Box(val value) type Wrapped = Box fun foo(factory: () -> Wrapped) = factory().value.oops //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.643: fun foo(factory: () -> Wrapped) = factory().value.oops +//│ ║ l.641: fun foo(factory: () -> Wrapped) = factory().value.oops //│ ║ ^^^^^^^^^^^^^^^^^^^^ //│ ╟── Cannot resolve member 'oops' of a value with unknown shape. //│ ╟── Constructor parameter 'value' has no type annotation. -//│ ║ l.641: class Box(val value) +//│ ║ l.639: class Box(val value) //│ ║ ^^^^^^^^^ //│ ╟── This type annotation does not provide a type for field 'value'. -//│ ║ l.643: fun foo(factory: () -> Wrapped) = factory().value.oops +//│ ║ l.641: fun foo(factory: () -> Wrapped) = factory().value.oops //│ ╙── ^^^^^^^ // TODO: ascription preserves receiver flow for the selected field's missing type. @@ -660,14 +658,14 @@ class Item with class Box(val value) (Box(new Item) as Box).value.oops //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.661: (Box(new Item) as Box).value.oops +//│ ║ l.659: (Box(new Item) as Box).value.oops //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Cannot resolve member 'oops' of a value with unknown shape. //│ ╟── Constructor parameter 'value' has no type annotation. -//│ ║ l.660: class Box(val value) +//│ ║ l.658: class Box(val value) //│ ║ ^^^^^^^^^ //│ ╟── This type annotation does not provide a type for field 'value'. -//│ ║ l.661: (Box(new Item) as Box).value.oops +//│ ║ l.659: (Box(new Item) as Box).value.oops //│ ╙── ^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ ═══[RUNTIME ERROR] Expected: '42', got: 'undefined' @@ -676,13 +674,13 @@ class Box(val value) :ge fun foo[A](factory: () -> [A, A]) = factory().0.oops //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.677: fun foo[A](factory: () -> [A, A]) = factory().0.oops +//│ ║ l.675: fun foo[A](factory: () -> [A, A]) = factory().0.oops //│ ║ ^^^^^^^^^^^^^^^^ //│ ╟── Cannot resolve member 'oops' of a value with unknown shape. -//│ ║ l.677: fun foo[A](factory: () -> [A, A]) = factory().0.oops +//│ ║ l.675: fun foo[A](factory: () -> [A, A]) = factory().0.oops //│ ║ ^ //│ ╟── Type parameter 'A' does not specify a member interface. -//│ ║ l.677: fun foo[A](factory: () -> [A, A]) = factory().0.oops +//│ ║ l.675: fun foo[A](factory: () -> [A, A]) = factory().0.oops //│ ╙── ^ // A concrete annotation supplies the nested member interface without losing tuple precision. @@ -699,14 +697,14 @@ foo(Box[Item]([new Item, new Item])) class Pair[A, B](val value: [A, B]) fun omittedSecond(x: Pair[Int]) = x.value.1.oops //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.700: fun omittedSecond(x: Pair[Int]) = x.value.1.oops +//│ ║ l.698: fun omittedSecond(x: Pair[Int]) = x.value.1.oops //│ ║ ^^^^^^^^^^^^^^ //│ ╟── Cannot resolve member 'oops' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.700: fun omittedSecond(x: Pair[Int]) = x.value.1.oops +//│ ║ l.698: fun omittedSecond(x: Pair[Int]) = x.value.1.oops //│ ║ ^^^^^^^^^^^^ //│ ╟── 'omittedSecond' is exposed by this compilation unit. -//│ ║ l.700: fun omittedSecond(x: Pair[Int]) = x.value.1.oops +//│ ║ l.698: fun omittedSecond(x: Pair[Int]) = x.value.1.oops //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ // An unfilled omitted alias argument likewise cannot justify an arbitrary member. @@ -714,12 +712,12 @@ fun omittedSecond(x: Pair[Int]) = x.value.1.oops type HalfPair[T] = Pair[Int, T] fun omittedAlias(x: HalfPair) = x.value.1.oops //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.715: fun omittedAlias(x: HalfPair) = x.value.1.oops +//│ ║ l.713: fun omittedAlias(x: HalfPair) = x.value.1.oops //│ ║ ^^^^^^^^^^^^^^ //│ ╟── Cannot resolve member 'oops' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.715: fun omittedAlias(x: HalfPair) = x.value.1.oops +//│ ║ l.713: fun omittedAlias(x: HalfPair) = x.value.1.oops //│ ║ ^^^^^^^^^^^ //│ ╟── 'omittedAlias' is exposed by this compilation unit. -//│ ║ l.715: fun omittedAlias(x: HalfPair) = x.value.1.oops +//│ ║ l.713: fun omittedAlias(x: HalfPair) = x.value.1.oops //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/newres/Functions.mls b/hkmc2/shared/src/test/mlscript/newres/Functions.mls index bb42812db7..2c04237801 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Functions.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Functions.mls @@ -11,7 +11,7 @@ foo() //│ ╟── Function 'foo' expected 1 argument, but got 0 //│ ║ l.4: private fun foo(x) = x + 1 //│ ╙── ^^^^^^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'foo' expected 1 argument but got 0 +//│ ═══[RUNTIME ERROR] Error: Function 'foo' expected at least 1 argument but got 0 :e foo(1, 2) @@ -21,7 +21,7 @@ foo(1, 2) //│ ╟── Function 'foo' expected 1 argument, but got 2 //│ ║ l.4: private fun foo(x) = x + 1 //│ ╙── ^^^^^^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'foo' expected 1 argument but got 2 +//│ = 2 :sr @@ -79,6 +79,11 @@ callid1(Predef) //│ | | | listenTerm: trm = member:callid1⁰ //│ | | | listen: trm = member:callid1⁰ //│ | | | listenBMS: bms = function +//│ | | | listenedBMS: bms = function +//│ | | | fromBMS: bms = member:callid1⁰, sh = member:callid1⁰, flow = callid1¹, markss = List() +//│ | | | .exit MarkedShape (member:callid1⁰, term:callid1², callid1¹) +//│ | | | = MarkedShape(member:callid1⁰, ↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | | PUSH MarkedShape(member:callid1⁰, ↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) //│ | | ~> member:callid1⁰ //│ | | Elab subterm Tup(List(Ident(Predef))) //│ | | | Elab term Ident(Predef) @@ -95,119 +100,114 @@ callid1(Predef) //│ | | listenTerm: trm = member:callid1⁰ //│ | | listen: trm = member:callid1⁰ //│ | | listenBMS: bms = function -//│ | ~> member:callid1⁰(member:Predef⁰) -//│ | listenedBMS: bms = function -//│ | fromBMS: bms = member:callid1⁰, sh = member:callid1⁰, flow = callid1¹, markss = List() -//│ | .exit MarkedShape (member:callid1⁰, term:callid1², callid1¹) -//│ | = MarkedShape(member:callid1⁰, ↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | PUSH MarkedShape(member:callid1⁰, ↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | listenedBMS: bms = function -//│ | fromBMS: bms = member:callid1⁰, sh = member:callid1⁰, flow = callid1¹, markss = List() -//│ | .exit MarkedShape (member:callid1⁰, term:callid1², callid1¹) -//│ | = MarkedShape(member:callid1⁰, ↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | PUSH MarkedShape(member:callid1⁰, ↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | appShape: lhs = MarkedShape(member:callid1⁰, ↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ), args = [member:Predef⁰], res = member:callid1⁰(member:Predef⁰) -//│ | listenTerm: trm = [member:Predef⁰] -//│ | listen: trm = [member:Predef⁰] -//│ | listenTerm: trm = member:Predef⁰ -//│ | listen: trm = member:Predef⁰ -//│ | listenBMS: bms = module -//│ | listenedBMS: bms = module -//│ | fromBMS: bms = member:Predef⁰, sh = module:Predef¹, flow = Predef², markss = List() -//│ | PUSH module:Predef¹ -//│ | .exit module:Predef¹ (ϵ) -//│ | = module:Predef¹ -//│ | .enter module:Predef¹ (↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | | .enter MarkedShape(module:Predef¹, ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) (ϵ) +//│ | | listenedBMS: bms = function +//│ | | fromBMS: bms = member:callid1⁰, sh = member:callid1⁰, flow = callid1¹, markss = List() +//│ | | .exit MarkedShape (member:callid1⁰, term:callid1², callid1¹) +//│ | | = MarkedShape(member:callid1⁰, ↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | PUSH MarkedShape(member:callid1⁰, ↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | appShape: lhs = MarkedShape(member:callid1⁰, ↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ), args = [member:Predef⁰], res = member:callid1⁰(member:Predef⁰) +//│ | | listenTerm: trm = [member:Predef⁰] +//│ | | listen: trm = [member:Predef⁰] +//│ | | listenTerm: trm = member:Predef⁰ +//│ | | listen: trm = member:Predef⁰ +//│ | | listenBMS: bms = module +//│ | | listenedBMS: bms = module +//│ | | fromBMS: bms = member:Predef⁰, sh = module:Predef¹, flow = Predef², markss = List() +//│ | | PUSH module:Predef¹ +//│ | | .exit module:Predef¹ (ϵ) +//│ | | = module:Predef¹ +//│ | | .enter module:Predef¹ (↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | | .enter MarkedShape(module:Predef¹, ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) (ϵ) +//│ | | | = MarkedShape(module:Predef¹, ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) //│ | | = MarkedShape(module:Predef¹, ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | = MarkedShape(module:Predef¹, ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | newSel: sel = p⁰.id, shape = MarkedShape(module:Predef¹, ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | newSel member: bms = member:id⁰, mss = List(↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | listenBMS: bms = function -//│ | listenedBMS: bms = function -//│ | fromBMS: bms = member:id⁰, sh = member:id⁰, flow = ⋅id⁰, markss = List(↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | .exit MarkedShape (member:id⁰, term:Predef¹/id¹, ⋅id⁰) -//│ | = MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | .exit MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | | .exit MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (ϵ) +//│ | | newSel: sel = p⁰.id, shape = MarkedShape(module:Predef¹, ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | newSel member: bms = member:id⁰, mss = List(↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | listenBMS: bms = function +//│ | | listenedBMS: bms = function +//│ | | fromBMS: bms = member:id⁰, sh = member:id⁰, flow = ⋅id⁰, markss = List(↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | .exit MarkedShape (member:id⁰, term:Predef¹/id¹, ⋅id⁰) //│ | | = MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | = MarkedShape(member:id⁰, ↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | PUSH MarkedShape(member:id⁰, ↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | listenBMS: bms = function -//│ | listenedBMS: bms = function -//│ | fromBMS: bms = member:id⁰, sh = member:id⁰, flow = ⋅id⁰, markss = List(↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | .exit MarkedShape (member:id⁰, term:Predef¹/id¹, ⋅id⁰) -//│ | = MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | .exit MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | | .exit MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (ϵ) +//│ | | .exit MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | | .exit MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (ϵ) +//│ | | | = MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | = MarkedShape(member:id⁰, ↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | PUSH MarkedShape(member:id⁰, ↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | listenBMS: bms = function +//│ | | listenedBMS: bms = function +//│ | | fromBMS: bms = member:id⁰, sh = member:id⁰, flow = ⋅id⁰, markss = List(↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | .exit MarkedShape (member:id⁰, term:Predef¹/id¹, ⋅id⁰) //│ | | = MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | = MarkedShape(member:id⁰, ↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | PUSH MarkedShape(member:id⁰, ↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | appShape: lhs = MarkedShape(member:id⁰, ↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ), args = [1], res = p⁰.id(1) -//│ | listenTerm: trm = [1] -//│ | 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⁰, ) +//│ | | .exit MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | | .exit MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (ϵ) +//│ | | | = MarkedShape(member:id⁰, ↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | = MarkedShape(member:id⁰, ↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | PUSH MarkedShape(member:id⁰, ↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | appShape: lhs = MarkedShape(member:id⁰, ↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ), args = [1], res = p⁰.id‹term:Predef¹/id¹›(1) +//│ | | listenTerm: trm = [1] +//│ | | 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⁰⟩ϵ) -//│ | | .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) -//│ | .enter IntroShape(1) (↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | | .exit MarkedShape (IntroShape(1), term:callid1², callid1¹) +//│ | | .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) -//│ | | .enter IntroShape(1) (↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | | | .enter MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (ϵ) +//│ | | .enter IntroShape(1) (↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | | .exit MarkedShape (IntroShape(1), term:callid1², callid1¹) +//│ | | | = IntroShape(1) +//│ | | | .enter IntroShape(1) (↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | | | .enter MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (ϵ) +//│ | | | | = MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) //│ | | | = MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) //│ | | = MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | = MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | appShape isSaturated? true; head? member:id⁰, List(↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | lhs false List([(x⁰), List(↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ)]) -//│ | appShape: td.body = Some(SimpleRef(x⁰)) -//│ | listenTerm: trm = x⁰ -//│ | listen: trm = x⁰ -//│ | .exit MarkedShape(UnknownValueShape(x⁰), ↘⟨term:Predef¹/id¹⟩%⟨exposed⁰⟩ϵ) (↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | | .exit MarkedShape(UnknownValueShape(x⁰), ↘⟨term:Predef¹/id¹⟩%⟨exposed⁰⟩ϵ) (↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | | | .exit MarkedShape(UnknownValueShape(x⁰), ↘⟨term:Predef¹/id¹⟩%⟨exposed⁰⟩ϵ) (ϵ) -//│ | | | = MarkedShape(UnknownValueShape(x⁰), ↘⟨term:Predef¹/id¹⟩%⟨exposed⁰⟩ϵ) -//│ | | | .exit MarkedShape (MarkedShape(UnknownValueShape(x⁰), ↘⟨term:Predef¹/id¹⟩%⟨exposed⁰⟩ϵ), term:Predef¹/id¹, ⋅id⁰) +//│ | | appShape isSaturated? true; head? member:id⁰, List(↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | lhs false List([(x⁰), List(↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ)]) +//│ | | appShape: td.body = Some(SimpleRef(x⁰)) +//│ | | listenTerm: trm = x⁰ +//│ | | listen: trm = x⁰ +//│ | | .exit MarkedShape(UnknownValueShape(x⁰), ↘⟨term:Predef¹/id¹⟩%⟨exposed⁰⟩ϵ) (↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | | .exit MarkedShape(UnknownValueShape(x⁰), ↘⟨term:Predef¹/id¹⟩%⟨exposed⁰⟩ϵ) (↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | | | .exit MarkedShape(UnknownValueShape(x⁰), ↘⟨term:Predef¹/id¹⟩%⟨exposed⁰⟩ϵ) (ϵ) +//│ | | | | = MarkedShape(UnknownValueShape(x⁰), ↘⟨term:Predef¹/id¹⟩%⟨exposed⁰⟩ϵ) +//│ | | | | .exit MarkedShape (MarkedShape(UnknownValueShape(x⁰), ↘⟨term:Predef¹/id¹⟩%⟨exposed⁰⟩ϵ), term:Predef¹/id¹, ⋅id⁰) +//│ | | | | = NoShape //│ | | | = NoShape //│ | | = NoShape -//│ | = NoShape -//│ | .exit MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | | .exit MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | | | .exit MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (ϵ) -//│ | | | = MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) -//│ | | | .exit MarkedShape (MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ), term:Predef¹/id¹, ⋅id⁰) +//│ | | .exit MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (↘⟨term:callid1²⟩%⟨callid1¹⟩↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | | .exit MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (↗⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | | | .exit MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) (ϵ) +//│ | | | | = MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ) +//│ | | | | .exit MarkedShape (MarkedShape(IntroShape(1), ↘⟨term:Predef¹/id¹⟩%⟨⋅id⁰⟩ϵ), term:Predef¹/id¹, ⋅id⁰) +//│ | | | | = IntroShape(1) //│ | | | = IntroShape(1) -//│ | | = IntroShape(1) -//│ | = MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | appShape isSaturated? true; head? member:callid1⁰, List(↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | lhs false List([(p⁰), List(↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ)]) -//│ | appShape: td.body = Some(App(NewSel(SimpleRef(p⁰),Ident(id),None),Tup(List(Fld(‹›,Lit(IntLit(1)),None))))) -//│ | listenTerm: trm = p⁰.id(1) -//│ | listen: trm = p⁰.id(1) -//│ | .exit MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) (↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | | .exit MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) (ϵ) //│ | | = MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) -//│ | | .exit MarkedShape (MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ), term:callid1², callid1¹) +//│ | | appShape isSaturated? true; head? member:callid1⁰, List(↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | lhs false List([(p⁰), List(↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ)]) +//│ | | appShape: td.body = Some(App(NewSel(SimpleRef(p⁰),Ident(id),None),Tup(List(Fld(‹›,Lit(IntLit(1)),None))))) +//│ | | listenTerm: trm = p⁰.id‹term:Predef¹/id¹›(1) +//│ | | listen: trm = p⁰.id‹term:Predef¹/id¹›(1) +//│ | | .exit MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) (↗⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | | .exit MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) (ϵ) +//│ | | | = MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ) +//│ | | | .exit MarkedShape (MarkedShape(IntroShape(1), ↘⟨term:callid1²⟩%⟨callid1¹⟩ϵ), term:callid1², callid1¹) +//│ | | | = IntroShape(1) //│ | | = IntroShape(1) -//│ | = IntroShape(1) -//│ ~> { fun member:callid1⁰(p⁰) = p⁰.id(1); member:callid1⁰(member:Predef⁰) } +//│ | ~> member:callid1⁰(member:Predef⁰) +//│ ~> { fun member:callid1⁰(p⁰) = p⁰.id‹term:Predef¹/id¹›(1); member:callid1⁰(member:Predef⁰) } //│ listenTerm: trm = member:callid1⁰(member:Predef⁰) //│ listen: trm = member:callid1⁰(member:Predef⁰) //│ ————————————————| Resolved |———————————————————————————————————————————————————————————————————————— diff --git a/hkmc2/shared/src/test/mlscript/newres/GenericMethods.mls b/hkmc2/shared/src/test/mlscript/newres/GenericMethods.mls index a140898f10..fb3b302e64 100644 --- a/hkmc2/shared/src/test/mlscript/newres/GenericMethods.mls +++ b/hkmc2/shared/src/test/mlscript/newres/GenericMethods.mls @@ -61,8 +61,8 @@ class Child extends Base with //│ ║ ^^^^^^^^^^^^^^^^^ //│ ╟── Function 'map' expected 1 type argument, but got 2 //│ ║ in ../decls/Prelude.mls -//│ ║ l.261: fun map[U](f: (T, Int, Array[T]) -> U): Array[U] -//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.282: fun map[U](f: (T, drop Int, drop Array[T]) -> U): Array[U] +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ = [1] // Imported free functions and methods use the same inference as prelude methods. diff --git a/hkmc2/shared/src/test/mlscript/newres/InferenceHoleScopes.mls b/hkmc2/shared/src/test/mlscript/newres/InferenceHoleScopes.mls index edcc1237b1..3bfdd3e101 100644 --- a/hkmc2/shared/src/test/mlscript/newres/InferenceHoleScopes.mls +++ b/hkmc2/shared/src/test/mlscript/newres/InferenceHoleScopes.mls @@ -72,9 +72,6 @@ call(Item(2)) //│ ║ l.61: fun make(x): Box = Box(x) //│ ║ ^^^^^^^^^^^^^^^^ //│ ╟── Cannot resolve member 'value' of a value with unknown shape. -//│ ║ l.60: class Box[T](val item: T) -//│ ║ ^ -//│ ╟── Cannot resolve member 'value' of a value with unknown shape. //│ ╟── Parameter 'box' admits values of unknown shape. //│ ║ l.62: fun read(box: Box) = box.item.value //│ ║ ^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/newres/InferenceHoles.mls b/hkmc2/shared/src/test/mlscript/newres/InferenceHoles.mls index 7d4e240148..11be7c139d 100644 --- a/hkmc2/shared/src/test/mlscript/newres/InferenceHoles.mls +++ b/hkmc2/shared/src/test/mlscript/newres/InferenceHoles.mls @@ -26,16 +26,16 @@ append(ys, Second(6), 2) [xs.0.first, ys.0.second] //│ ╔══[COMPILATION ERROR] Resolution error in selection //│ ║ l.26: [xs.0.first, ys.0.second] -//│ ║ ^^^^^^^^^^^ -//│ ╟── Instance of class 'First' does not contain member 'second' -//│ ║ l.24: append(xs, First(5), 2) -//│ ╙── ^^^^^^^^ -//│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.26: [xs.0.first, ys.0.second] //│ ║ ^^^^^^^^^^ //│ ╟── Instance of class 'Second' does not contain member 'first' //│ ║ l.25: append(ys, Second(6), 2) //│ ╙── ^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.26: [xs.0.first, ys.0.second] +//│ ║ ^^^^^^^^^^^ +//│ ╟── Instance of class 'First' does not contain member 'second' +//│ ║ l.24: append(xs, First(5), 2) +//│ ╙── ^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ ═══[RUNTIME ERROR] Expected: '[5, 6]', got: 'undefined' //│ xs = [First(5)] diff --git a/hkmc2/shared/src/test/mlscript/newres/InterfaceExposure.mls b/hkmc2/shared/src/test/mlscript/newres/InterfaceExposure.mls index c1eac35399..e8b1a1b3bb 100644 --- a/hkmc2/shared/src/test/mlscript/newres/InterfaceExposure.mls +++ b/hkmc2/shared/src/test/mlscript/newres/InterfaceExposure.mls @@ -88,8 +88,6 @@ apply(x => x) //│ ║ l.85: fun apply(f) = f(1) //│ ║ ^^^^ //│ ╟── Value of unknown shape cannot be called like a function. -//│ ║ l.85: fun apply(f) = f(1) -//│ ║ ^ //│ ╟── Parameter 'f' admits values of unknown shape. //│ ║ l.85: fun apply(f) = f(1) //│ ║ ^ @@ -108,17 +106,17 @@ apply(x => x) :e fun escaped() = x => x.a //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.109: fun escaped() = x => x.a +//│ ║ l.107: fun escaped() = x => x.a //│ ║ ^^^ //│ ╟── Cannot resolve member 'a' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.109: fun escaped() = x => x.a +//│ ║ l.107: fun escaped() = x => x.a //│ ║ ^ //│ ╟── This value is returned here. -//│ ║ l.109: fun escaped() = x => x.a +//│ ║ l.107: fun escaped() = x => x.a //│ ║ ^^^^^^^^ //│ ╟── 'escaped' is exposed by this compilation unit. -//│ ║ l.109: fun escaped() = x => x.a +//│ ║ l.107: fun escaped() = x => x.a //│ ╙── ^^^^^^^^^^^^^ :e @@ -126,17 +124,17 @@ private fun hidden(x) = x.a val escaped = [hidden] hidden({a: 1}) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.125: private fun hidden(x) = x.a +//│ ║ l.123: private fun hidden(x) = x.a //│ ║ ^^^ //│ ╟── Cannot resolve member 'a' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.125: private fun hidden(x) = x.a +//│ ║ l.123: private fun hidden(x) = x.a //│ ║ ^ //│ ╟── This value is stored in this tuple. -//│ ║ l.126: val escaped = [hidden] +//│ ║ l.124: val escaped = [hidden] //│ ║ ^^^^^^^^ //│ ╟── 'escaped' is exposed by this compilation unit. -//│ ║ l.126: val escaped = [hidden] +//│ ║ l.124: val escaped = [hidden] //│ ╙── ^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ escaped = [fun hidden] @@ -146,17 +144,17 @@ private fun hidden(x) = x.a val escaped = {f: hidden} hidden({a: 1}) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.145: private fun hidden(x) = x.a +//│ ║ l.143: private fun hidden(x) = x.a //│ ║ ^^^ //│ ╟── Cannot resolve member 'a' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.145: private fun hidden(x) = x.a +//│ ║ l.143: private fun hidden(x) = x.a //│ ║ ^ //│ ╟── This value is stored in this record field. -//│ ║ l.146: val escaped = {f: hidden} +//│ ║ l.144: val escaped = {f: hidden} //│ ║ ^ //│ ╟── 'escaped' is exposed by this compilation unit. -//│ ║ l.146: val escaped = {f: hidden} +//│ ║ l.144: val escaped = {f: hidden} //│ ╙── ^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ escaped = {f: fun hidden} @@ -181,20 +179,20 @@ private fun hiddenRecord(x) = x.a fun exportRecord() = {callback: hiddenRecord} hiddenRecord({a: 1}) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.180: private fun hiddenRecord(x) = x.a +//│ ║ l.178: private fun hiddenRecord(x) = x.a //│ ║ ^^^ //│ ╟── Cannot resolve member 'a' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.180: private fun hiddenRecord(x) = x.a +//│ ║ l.178: private fun hiddenRecord(x) = x.a //│ ║ ^ //│ ╟── This value is stored in this record field. -//│ ║ l.181: fun exportRecord() = {callback: hiddenRecord} +//│ ║ l.179: fun exportRecord() = {callback: hiddenRecord} //│ ║ ^^^^^^^^ //│ ╟── This value is returned here. -//│ ║ l.181: fun exportRecord() = {callback: hiddenRecord} +//│ ║ l.179: fun exportRecord() = {callback: hiddenRecord} //│ ║ ^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── 'exportRecord' is exposed by this compilation unit. -//│ ║ l.181: fun exportRecord() = {callback: hiddenRecord} +//│ ║ l.179: fun exportRecord() = {callback: hiddenRecord} //│ ╙── ^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -206,14 +204,14 @@ class CopyBox(val value) with fun copy() = CopyBox(value) CopyBox({answer: 42}).copy().value.answer //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.207: CopyBox({answer: 42}).copy().value.answer +//│ ║ l.205: CopyBox({answer: 42}).copy().value.answer //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Cannot resolve member 'answer' of a value with unknown shape. //│ ╟── Parameter 'value' admits values of unknown shape. -//│ ║ l.205: class CopyBox(val value) with +//│ ║ l.203: class CopyBox(val value) with //│ ║ ^^^^^^^^^ //│ ╟── 'CopyBox' is exposed by this compilation unit. -//│ ║ l.205: class CopyBox(val value) with +//│ ║ l.203: class CopyBox(val value) with //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ ═══[RUNTIME ERROR] Expected: '42', got: 'undefined' @@ -226,17 +224,17 @@ oldHidden({a: 1}) :e val oldEscape = {callback: oldHidden} //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.222: private fun oldHidden(x) = x.a +//│ ║ l.220: private fun oldHidden(x) = x.a //│ ║ ^^^ //│ ╟── Cannot resolve member 'a' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.222: private fun oldHidden(x) = x.a +//│ ║ l.220: private fun oldHidden(x) = x.a //│ ║ ^ //│ ╟── This value is stored in this record field. -//│ ║ l.227: val oldEscape = {callback: oldHidden} +//│ ║ l.225: val oldEscape = {callback: oldHidden} //│ ║ ^^^^^^^^ //│ ╟── 'oldEscape' is exposed by this compilation unit. -//│ ║ l.227: val oldEscape = {callback: oldHidden} +//│ ║ l.225: val oldEscape = {callback: oldHidden} //│ ╙── ^^^^^^^^^^^^^ //│ oldEscape = {callback: fun oldHidden} @@ -245,16 +243,16 @@ val oldEscape = {callback: oldHidden} fun spreadRecord[A](x: A) = ({...x}).a spreadRecord({a: 1}) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.245: fun spreadRecord[A](x: A) = ({...x}).a +//│ ║ l.243: fun spreadRecord[A](x: A) = ({...x}).a //│ ║ ^^^^^^^^^^ //│ ╟── This spread has no known element shape. -//│ ║ l.245: fun spreadRecord[A](x: A) = ({...x}).a +//│ ║ l.243: fun spreadRecord[A](x: A) = ({...x}).a //│ ║ ^ //│ ╟── Cannot resolve member 'a' across a computed key or unknown record spread. -//│ ║ l.245: fun spreadRecord[A](x: A) = ({...x}).a +//│ ║ l.243: fun spreadRecord[A](x: A) = ({...x}).a //│ ║ ^ //│ ╟── Type parameter 'A' does not specify a member interface. -//│ ║ l.245: fun spreadRecord[A](x: A) = ({...x}).a +//│ ║ l.243: fun spreadRecord[A](x: A) = ({...x}).a //│ ╙── ^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -263,16 +261,16 @@ spreadRecord({a: 1}) fun spreadTuple[A](x: A) = [...x].0.a spreadTuple([{a: 1}]) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.263: fun spreadTuple[A](x: A) = [...x].0.a +//│ ║ l.261: fun spreadTuple[A](x: A) = [...x].0.a //│ ║ ^^^^^^^^^^ //│ ╟── This spread has no known element shape. -//│ ║ l.263: fun spreadTuple[A](x: A) = [...x].0.a +//│ ║ l.261: fun spreadTuple[A](x: A) = [...x].0.a //│ ║ ^ //│ ╟── Cannot resolve member 'a' of a value with unknown shape. -//│ ║ l.263: fun spreadTuple[A](x: A) = [...x].0.a +//│ ║ l.261: fun spreadTuple[A](x: A) = [...x].0.a //│ ║ ^ //│ ╟── Type parameter 'A' does not specify a member interface. -//│ ║ l.263: fun spreadTuple[A](x: A) = [...x].0.a +//│ ║ l.261: fun spreadTuple[A](x: A) = [...x].0.a //│ ╙── ^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -287,20 +285,20 @@ private fun recursiveHidden(x) = x.a fun recursiveExport(n) = if n == 0 then {callback: recursiveHidden} else recursiveExport(n - 1) recursiveHidden({a: 1}) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.286: private fun recursiveHidden(x) = x.a +//│ ║ l.284: private fun recursiveHidden(x) = x.a //│ ║ ^^^ //│ ╟── Cannot resolve member 'a' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.286: private fun recursiveHidden(x) = x.a +//│ ║ l.284: private fun recursiveHidden(x) = x.a //│ ║ ^ //│ ╟── This value is stored in this record field. -//│ ║ l.287: fun recursiveExport(n) = if n == 0 then {callback: recursiveHidden} else recursiveExport(n - 1) +//│ ║ l.285: fun recursiveExport(n) = if n == 0 then {callback: recursiveHidden} else recursiveExport(n - 1) //│ ║ ^^^^^^^^ //│ ╟── This value is returned here. -//│ ║ l.287: fun recursiveExport(n) = if n == 0 then {callback: recursiveHidden} else recursiveExport(n - 1) +//│ ║ l.285: fun recursiveExport(n) = if n == 0 then {callback: recursiveHidden} else recursiveExport(n - 1) //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── 'recursiveExport' is exposed by this compilation unit. -//│ ║ l.287: fun recursiveExport(n) = if n == 0 then {callback: recursiveHidden} else recursiveExport(n - 1) +//│ ║ l.285: fun recursiveExport(n) = if n == 0 then {callback: recursiveHidden} else recursiveExport(n - 1) //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -311,23 +309,23 @@ class Shadowed { } fun Shadowed() = 0 //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.310: fun bad(x) = x.a +//│ ║ l.308: fun bad(x) = x.a //│ ║ ^^^ //│ ╟── Cannot resolve member 'a' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.310: fun bad(x) = x.a +//│ ║ l.308: fun bad(x) = x.a //│ ║ ^ //│ ╟── Member 'bad' is accessible here. -//│ ║ l.310: fun bad(x) = x.a +//│ ║ l.308: fun bad(x) = x.a //│ ║ ^^^^^^^^^^ //│ ╟── 'Shadowed' is exposed by this compilation unit. -//│ ║ l.309: class Shadowed { +//│ ║ l.307: class Shadowed { //│ ║ ^^^^^^^^^^^^^^^^ -//│ ║ l.310: fun bad(x) = x.a +//│ ║ l.308: fun bad(x) = x.a //│ ║ ^^^^^^^^^^^^^^^^^^ -//│ ║ l.311: } +//│ ║ l.309: } //│ ║ ^ -//│ ║ l.312: fun Shadowed() = 0 +//│ ║ l.310: fun Shadowed() = 0 //│ ╙── ^^^^^^^^^^^^^^ // A constructor test narrows the class, but cannot close its unannotated fields. @@ -336,17 +334,17 @@ class Holder(val value) fun inspect(x) = if x is Holder(y) then y.a else 0 inspect(Holder({a: 1})) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.336: fun inspect(x) = if x is Holder(y) then y.a else 0 +//│ ║ l.334: fun inspect(x) = if x is Holder(y) then y.a else 0 //│ ║ ^^^ //│ ╟── Cannot resolve member 'a' of a value with unknown shape. //│ ╟── This constructor field may contain a value of unknown shape. -//│ ║ l.335: class Holder(val value) +//│ ║ l.333: class Holder(val value) //│ ║ ^^^^^ //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.336: fun inspect(x) = if x is Holder(y) then y.a else 0 +//│ ║ l.334: fun inspect(x) = if x is Holder(y) then y.a else 0 //│ ║ ^ //│ ╟── 'inspect' is exposed by this compilation unit. -//│ ║ l.336: fun inspect(x) = if x is Holder(y) then y.a else 0 +//│ ║ l.334: fun inspect(x) = if x is Holder(y) then y.a else 0 //│ ╙── ^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -364,17 +362,17 @@ class GenericHolder[A](val value: A) fun inspectGeneric(x) = if x is GenericHolder(y) then y.a else 0 inspectGeneric(GenericHolder({a: 1})) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.364: fun inspectGeneric(x) = if x is GenericHolder(y) then y.a else 0 +//│ ║ l.362: fun inspectGeneric(x) = if x is GenericHolder(y) then y.a else 0 //│ ║ ^^^ //│ ╟── Cannot resolve member 'a' of a value with unknown shape. //│ ╟── This constructor field may contain a value of unknown shape. -//│ ║ l.363: class GenericHolder[A](val value: A) +//│ ║ l.361: class GenericHolder[A](val value: A) //│ ║ ^^^^^ //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.364: fun inspectGeneric(x) = if x is GenericHolder(y) then y.a else 0 +//│ ║ l.362: fun inspectGeneric(x) = if x is GenericHolder(y) then y.a else 0 //│ ║ ^ //│ ╟── 'inspectGeneric' is exposed by this compilation unit. -//│ ║ l.364: fun inspectGeneric(x) = if x is GenericHolder(y) then y.a else 0 +//│ ║ l.362: fun inspectGeneric(x) = if x is GenericHolder(y) then y.a else 0 //│ ╙── ^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -409,17 +407,17 @@ private val callbacks = mut [hiddenInSpread] val escapedSpread = [...callbacks] hiddenInSpread({a: 1}) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.407: private fun hiddenInSpread(x) = x.a +//│ ║ l.405: private fun hiddenInSpread(x) = x.a //│ ║ ^^^ //│ ╟── Cannot resolve member 'a' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ l.407: private fun hiddenInSpread(x) = x.a +//│ ║ l.405: private fun hiddenInSpread(x) = x.a //│ ║ ^ //│ ╟── This value is stored in this tuple. -//│ ║ l.409: val escapedSpread = [...callbacks] +//│ ║ l.407: val escapedSpread = [...callbacks] //│ ║ ^^^^^^^^^ //│ ╟── 'escapedSpread' is exposed by this compilation unit. -//│ ║ l.409: val escapedSpread = [...callbacks] +//│ ║ l.407: val escapedSpread = [...callbacks] //│ ╙── ^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ callbacks = [fun hiddenInSpread] diff --git a/hkmc2/shared/src/test/mlscript/newres/LegacyGenericMethods.mls b/hkmc2/shared/src/test/mlscript/newres/LegacyGenericMethods.mls deleted file mode 100644 index 31495f0c40..0000000000 --- a/hkmc2/shared/src/test/mlscript/newres/LegacyGenericMethods.mls +++ /dev/null @@ -1,19 +0,0 @@ -:. - -import "../../mlscript-compile/FingerTreeList.mls" - -// Calling a generic method imported from a legacy fixture must preserve the -// class's type argument when interpreting the method's annotated result. -:expect 42 -FingerTreeList.mk(42).at(0) -//│ = 42 - -import "../../mlscript-compile/LegacyGenericLibrary.mls" - -:expect [7, "other"] -class NumberBox(val number: Int) -class TextBox(val text: Str) -fun readNumber(cell: LegacyGenericLibrary.Cell[NumberBox]) = cell.get.number -fun readText(cell: LegacyGenericLibrary.Cell[TextBox]) = cell.getAt(0).text -[readNumber(LegacyGenericLibrary.Cell(NumberBox(7))), readText(LegacyGenericLibrary.Cell(TextBox("other")))] -//│ = [7, "other"] diff --git a/hkmc2/shared/src/test/mlscript/newres/MigratedFixtureInterfaces.mls b/hkmc2/shared/src/test/mlscript/newres/MigratedFixtureInterfaces.mls index 6cde48ae71..9f9407ed73 100644 --- a/hkmc2/shared/src/test/mlscript/newres/MigratedFixtureInterfaces.mls +++ b/hkmc2/shared/src/test/mlscript/newres/MigratedFixtureInterfaces.mls @@ -33,7 +33,7 @@ Runtime.Tuple.slice([1, 2, 3, 4], 1, 1) fun lookup(map: WeakMap[Object, Str], key: Object) = if map.get(key) is Str as value then value else "missing" -val key = new Object +val key = new Object() val map = new WeakMap map.set(key, "value") lookup(map, key) @@ -44,7 +44,7 @@ lookup(map, key) // This runtime primitive preserves undefined; user-call instrumentation forbids returning it. :noSanityCheck :expect true -Runtime.Tuple.get([undefined], 0) is undefined +Runtime.Tuple.get([unsafe.undefined], 0) is unsafe.undefined //│ = true // Tuple runtime operations also support lazy views. @@ -64,11 +64,11 @@ Term.show(Term.Lam([termSymbol], Term.Ref(termSymbol)), termContext) //│ = "(x_0) =>\n x_0" :expect ["null", "undefined", "true", "42"] -[null, undefined, true, 42].map((lit, ...rest) => Term.show(Term.Lit(lit), termContext)) +[null, unsafe.undefined, true, 42].map((lit, ...rest) => Term.show(Term.Lit(lit), termContext)) //│ = ["null", "undefined", "true", "42"] :expect "undefined" -Term.showPattern(Term.LitPattern(undefined), termContext) +Term.showPattern(Term.LitPattern(unsafe.undefined), termContext) //│ = "undefined" // Shared read interfaces retain element types without invariant Array[Any] annotations. @@ -84,7 +84,7 @@ fun iteratedValue(xs: IterableIndexable[Item]) = xs.iterator().next().value.valu :expect 7 class SpecialItem extends Item(7) fun iteratorValue(xs: Iterator[Item]) = xs.next().value.value -val items: Set[SpecialItem] = new Set +val items: Set[SpecialItem] = new Set() items.add(new SpecialItem) val specialized: Iterator[SpecialItem] = items.values() iteratorValue(specialized) diff --git a/hkmc2/shared/src/test/mlscript/newres/NegatedPatterns.mls b/hkmc2/shared/src/test/mlscript/newres/NegatedPatterns.mls index 85980eda74..dfc2919026 100644 --- a/hkmc2/shared/src/test/mlscript/newres/NegatedPatterns.mls +++ b/hkmc2/shared/src/test/mlscript/newres/NegatedPatterns.mls @@ -19,9 +19,9 @@ fun nullableLength(value: Str | null) = // Other singleton literals use the same refinement rule. :expect [3, 0] -fun definedLength(value: Str | undefined) = - if value is ~undefined as text then text.length else 0 -[definedLength("abc"), definedLength(undefined)] +fun definedLength(value: Str | unsafe.undefined) = + if value is ~unsafe.undefined as text then text.length else 0 +[definedLength("abc"), definedLength(unsafe.undefined)] //│ = [3, 0] :expect [3, 0] @@ -42,9 +42,9 @@ fun boxLength(value: TextBox | Empty) = // De Morgan's laws apply to both alternatives and conjunctions. :expect [3, 0, 0] -fun presentLength(value: Str | null | undefined) = - if value is ~(null | undefined) as text then text.length else 0 -[presentLength("abc"), presentLength(null), presentLength(undefined)] +fun presentLength(value: Str | null | unsafe.undefined) = + if value is ~(null | unsafe.undefined) as text then text.length else 0 +[presentLength("abc"), presentLength(null), presentLength(unsafe.undefined)] //│ = [3, 0, 0] :expect [3, 0] @@ -91,16 +91,16 @@ fun extractedLength(value: TextBox) = extractedLength(TextBox("abc")) //│ = 3 -// Excluding null does not remove undefined or manufacture an interface for Any. +// Excluding null does not remove false or manufacture an interface for Any. :ge -fun stillUndefined(value: Str | null | undefined) = +fun stillFalse(value: Str | null | false) = if value is ~null as text then text.length else 0 //│ ╔══[COMPILATION ERROR] Resolution error in selection //│ ║ l.97: if value is ~null as text then text.length else 0 //│ ║ ^^^^^^^^^^^ -//│ ╟── Undefined literal does not contain member 'length' -//│ ║ l.96: fun stillUndefined(value: Str | null | undefined) = -//│ ╙── ^^^^^^^^^ +//│ ╟── Boolean literal does not contain member 'length' +//│ ║ l.96: fun stillFalse(value: Str | null | false) = +//│ ╙── ^^^^^ :ge fun stillUnknown(value: Any) = diff --git a/hkmc2/shared/src/test/mlscript/newres/ObjectMethods.mls b/hkmc2/shared/src/test/mlscript/newres/ObjectMethods.mls index f90fcfcf3d..449341d1af 100644 --- a/hkmc2/shared/src/test/mlscript/newres/ObjectMethods.mls +++ b/hkmc2/shared/src/test/mlscript/newres/ObjectMethods.mls @@ -28,7 +28,7 @@ Empty.toString(1) //│ ║ ^^^^^^^^^^^^^^^^^ //│ ╟── Function 'toString' expected 0 arguments, but got 1 //│ ║ in ../decls/Prelude.mls -//│ ║ l.7: fun toString(): Str +//│ ║ l.8: fun toString(): Str //│ ╙── ^^^^^^^^^^^^^^^^^^^ //│ = "Empty" diff --git a/hkmc2/shared/src/test/mlscript/newres/Overrides.mls b/hkmc2/shared/src/test/mlscript/newres/Overrides.mls index 4bfd3dfdfe..340310a7f7 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Overrides.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Overrides.mls @@ -92,7 +92,7 @@ new SuperChild().foo(3) //│ method foo³ = fun foo⁴(x) { //│ let tmp1, tmp2; //│ set tmp1 = +⁰(x, 1); -//│ set tmp2 = super[SuperBase¹](SuperChild¹.this).foo²(tmp1); +//│ set tmp2 = super[SuperBase¹](SuperChild¹.this).["foo"](tmp1); //│ return *⁰(tmp2, 2) //│ } //│ }; @@ -434,7 +434,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:3360' +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'NewResolver.scala:3689' //│ ╟── The compilation result may be incorrect. //│ ╙── This is a compiler bug; please report it to the maintainers. //│ = 4 @@ -449,7 +449,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:3360' +//│ ╔══[INTERNAL ERROR] Compiler reached an unexpected state at 'NewResolver.scala:3689' //│ ╟── 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/PartialSignatures.mls b/hkmc2/shared/src/test/mlscript/newres/PartialSignatures.mls index e25849b859..1dc9ea019f 100644 --- a/hkmc2/shared/src/test/mlscript/newres/PartialSignatures.mls +++ b/hkmc2/shared/src/test/mlscript/newres/PartialSignatures.mls @@ -86,16 +86,16 @@ append(ys, Second(6)) [xs.0.first, ys.0.second] //│ ╔══[COMPILATION ERROR] Resolution error in selection //│ ║ l.86: [xs.0.first, ys.0.second] -//│ ║ ^^^^^^^^^^^ -//│ ╟── Instance of class 'First' does not contain member 'second' -//│ ║ l.84: append(xs, First(5)) -//│ ╙── ^^^^^^^^ -//│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.86: [xs.0.first, ys.0.second] //│ ║ ^^^^^^^^^^ //│ ╟── Instance of class 'Second' does not contain member 'first' //│ ║ l.85: append(ys, Second(6)) //│ ╙── ^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.86: [xs.0.first, ys.0.second] +//│ ║ ^^^^^^^^^^^ +//│ ╟── Instance of class 'First' does not contain member 'second' +//│ ║ l.84: append(xs, First(5)) +//│ ╙── ^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ ═══[RUNTIME ERROR] Expected: '[5, 6]', got: 'undefined' //│ xs = [First(5)] diff --git a/hkmc2/shared/src/test/mlscript/newres/PatternDefinitionBindings.mls b/hkmc2/shared/src/test/mlscript/newres/PatternDefinitionBindings.mls index 357091b4fa..f20a52d9b5 100644 --- a/hkmc2/shared/src/test/mlscript/newres/PatternDefinitionBindings.mls +++ b/hkmc2/shared/src/test/mlscript/newres/PatternDefinitionBindings.mls @@ -99,7 +99,7 @@ pattern Bad = ((Str as s => s.length) as n) => n.toUpperCase() //│ ║ ^^^^^^^^^^^^^ //│ ╟── Value of type 'Int' does not contain member 'toUpperCase' //│ ║ in ../decls/Prelude.mls -//│ ║ l.206: val length: Int +//│ ║ l.226: val length: Int //│ ╙── ^^^ // Conjunction produces a pair, not the string tested by its left operand. diff --git a/hkmc2/shared/src/test/mlscript/newres/PatternRefinement.mls b/hkmc2/shared/src/test/mlscript/newres/PatternRefinement.mls index d28ea96d2b..753fecbbff 100644 --- a/hkmc2/shared/src/test/mlscript/newres/PatternRefinement.mls +++ b/hkmc2/shared/src/test/mlscript/newres/PatternRefinement.mls @@ -47,3 +47,32 @@ class Buffer(val items: Array[Int]) with fun length = items.length Buffer([1, 2, 3]).read(Buffer([4, 5, 6])) //│ = 3 + +// A class returned from a generic factory retains the factory's type argument +// when a pattern refines an unknown input to that class's declared fields. +class First(val first: Int) +class Second(val second: Int) +private fun makeBox[A](item: A) = + class Box(val value: A) + Box +val FirstBox = makeBox(First(1)) +val SecondBox = makeBox(Second(2)) +//│ FirstBox = fun Box { class: class Box } +//│ SecondBox = fun Box { class: class Box } + +:expect 3 +fun readFirst(value) = if value is FirstBox(item) then item.first else 0 +readFirst(FirstBox(First(3))) +//│ = 3 + +:expect 4 +fun readSecond(value) = if value is SecondBox(item) then item.second else 0 +readSecond(SecondBox(Second(4))) +//│ = 4 + +// The two calls to makeBox must not contribute field types to one another. +:e +fun wrong(value) = if value is FirstBox(item) then item.second else 0 +//│ ╔══[COMPILATION ERROR] This selection of member 'second' has no resolved target +//│ ║ l.75: fun wrong(value) = if value is FirstBox(item) then item.second else 0 +//│ ╙── ^^^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/newres/PrimitiveMembers.mls b/hkmc2/shared/src/test/mlscript/newres/PrimitiveMembers.mls index bf55343201..59d1d853b0 100644 --- a/hkmc2/shared/src/test/mlscript/newres/PrimitiveMembers.mls +++ b/hkmc2/shared/src/test/mlscript/newres/PrimitiveMembers.mls @@ -22,7 +22,7 @@ fun decimal(n: Num) = n.toFixed(1) //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. :expect [true, 2, false, 1] -let values = new mut Set +let values = new mut Set() values.add("a") values.add("b") let present = values.has("a") diff --git a/hkmc2/shared/src/test/mlscript/newres/Projections.mls b/hkmc2/shared/src/test/mlscript/newres/Projections.mls index dfc212aa8a..69273e58a0 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Projections.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Projections.mls @@ -76,11 +76,11 @@ fun ambiguous(a) = a.Item#get() //│ ║ l.74: fun ambiguous(a) = a.Item#get() //│ ║ ^^^^^ //│ ╟── class: 'Item' -//│ ║ l.71: module Right { class Item extends Parent {} } -//│ ║ ^^^^ -//│ ╟── class: 'Item' //│ ║ l.70: module Left { class Item extends Parent {} } -//│ ╙── ^^^^ +//│ ║ ^^^^ +//│ ╟── class: 'Item' +//│ ║ l.71: module Right { class Item extends Parent {} } +//│ ╙── ^^^^ // Forwarded arguments carry shapes through the projected method's result. // FIXME: The projected wrapper loses the activation of its exposed argument. diff --git a/hkmc2/shared/src/test/mlscript/newres/PromiseSignatures.mls b/hkmc2/shared/src/test/mlscript/newres/PromiseSignatures.mls index c833515ff5..1ffd347293 100644 --- a/hkmc2/shared/src/test/mlscript/newres/PromiseSignatures.mls +++ b/hkmc2/shared/src/test/mlscript/newres/PromiseSignatures.mls @@ -57,7 +57,7 @@ Promise.any([Promise.reject("failed"), Promise.resolve({answer: 7})]).then(value // An absent rejection handler contributes no additional fulfillment candidate. :await :expect 8 -Promise.resolve({answer: 8}).then(value => value, undefined).then(value => value.answer) +Promise.resolve({answer: 8}).then(value => value, unsafe.undefined).then(value => value.answer) //│ = 8 :await @@ -68,18 +68,18 @@ Promise.resolve({answer: 9}).then(value => value, null).then(value => value.answ // A promise used only as a completion signal has an undefined payload. :await :expect true -(new Promise[undefined]((resolve, reject) => resolve(undefined))).then(value => value === undefined) +(new Promise[unsafe.undefined]((resolve, reject) => resolve(unsafe.undefined))).then(value => value === unsafe.undefined) //│ = true // Explicitly absent catch/finally callbacks preserve the original settlement. :await :expect 14 -Promise.resolve({answer: 14}).catch(null).finally(undefined).then(value => value.answer) +Promise.resolve({answer: 14}).catch(null).finally(unsafe.undefined).then(value => value.answer) //│ = 14 :await :re -Promise.reject("still rejected").catch(undefined).finally(null) +Promise.reject("still rejected").catch(unsafe.undefined).finally(null) //│ ═══[RUNTIME ERROR] still rejected //│ = Promise {} @@ -87,34 +87,26 @@ Promise.reject("still rejected").catch(undefined).finally(null) :!js :e new Promise((resolve, reject) => resolve(10).missing) -//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ╔══[COMPILATION ERROR] This selection of member 'missing' has no resolved target //│ ║ l.89: new Promise((resolve, reject) => resolve(10).missing) -//│ ║ ^^^^^^^^^^^^^^^^^^^ -//│ ╟── Undefined literal does not contain member 'missing' -//│ ║ in ../decls/Prelude.mls -//│ ║ l.185: declare class Promise[out A](executor: ((A | Promise[A]) -> undefined, Any -> undefined) -> undefined) { -//│ ╙── ^^^^^^^^^ +//│ ╙── ^^^^^^^^^^^^^^^^^^^ :!js :e new Promise((resolve, reject) => reject("failed").missing) -//│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.100: new Promise((resolve, reject) => reject("failed").missing) -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^ -//│ ╟── Undefined literal does not contain member 'missing' -//│ ║ in ../decls/Prelude.mls -//│ ║ l.185: declare class Promise[out A](executor: ((A | Promise[A]) -> undefined, Any -> undefined) -> undefined) { -//│ ╙── ^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] This selection of member 'missing' has no resolved target +//│ ║ l.96: new Promise((resolve, reject) => reject("failed").missing) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^ // Both then branches contribute to the resulting promise type. :!js :e Promise.resolve({answer: 11}).then(value => value, reason => 0).then(value => value.answer) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.112: Promise.resolve({answer: 11}).then(value => value, reason => 0).then(value => value.answer) +//│ ║ l.104: Promise.resolve({answer: 11}).then(value => value, reason => 0).then(value => value.answer) //│ ║ ^^^^^^^^^^^^ //│ ╟── Integer literal does not contain member 'answer' -//│ ║ l.112: Promise.resolve({answer: 11}).then(value => value, reason => 0).then(value => value.answer) +//│ ║ l.104: Promise.resolve({answer: 11}).then(value => value, reason => 0).then(value => value.answer) //│ ╙── ^ // A rejection handler's output does not replace the fulfilled input type. @@ -122,10 +114,10 @@ Promise.resolve({answer: 11}).then(value => value, reason => 0).then(value => va :e Promise.resolve(0).catch(reason => Promise.resolve({answer: 12})).then(value => value.answer) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.123: Promise.resolve(0).catch(reason => Promise.resolve({answer: 12})).then(value => value.answer) +//│ ║ l.115: Promise.resolve(0).catch(reason => Promise.resolve({answer: 12})).then(value => value.answer) //│ ║ ^^^^^^^^^^^^ //│ ╟── Integer literal does not contain member 'answer' -//│ ║ l.123: Promise.resolve(0).catch(reason => Promise.resolve({answer: 12})).then(value => value.answer) +//│ ║ l.115: Promise.resolve(0).catch(reason => Promise.resolve({answer: 12})).then(value => value.answer) //│ ╙── ^ // A finalizer's return value does not become the promise's fulfillment value. @@ -133,10 +125,10 @@ Promise.resolve(0).catch(reason => Promise.resolve({answer: 12})).then(value => :e Promise.resolve(0).finally(() => console.log("finally")).then(value => value.answer) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.134: Promise.resolve(0).finally(() => console.log("finally")).then(value => value.answer) +//│ ║ l.126: Promise.resolve(0).finally(() => console.log("finally")).then(value => value.answer) //│ ║ ^^^^^^^^^^^^ //│ ╟── Integer literal does not contain member 'answer' -//│ ║ l.134: Promise.resolve(0).finally(() => console.log("finally")).then(value => value.answer) +//│ ║ l.126: Promise.resolve(0).finally(() => console.log("finally")).then(value => value.answer) //│ ╙── ^ // The bottom type stays empty through aliases used by promise interfaces. diff --git a/hkmc2/shared/src/test/mlscript/newres/Promises.mls b/hkmc2/shared/src/test/mlscript/newres/Promises.mls index c3697e7d54..13cbf18c32 100644 --- a/hkmc2/shared/src/test/mlscript/newres/Promises.mls +++ b/hkmc2/shared/src/test/mlscript/newres/Promises.mls @@ -456,8 +456,8 @@ val escapedCallback = @annotations.async {value => value.answer} //│ ║ ^^^^^ //│ ╟── This value is fulfilled by this promise. //│ ║ in ../decls/Prelude.mls -//│ ║ l.185: declare class Promise[out A](executor: ((A | Promise[A]) -> undefined, Any -> undefined) -> undefined) { -//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.204: declare class Promise[out A](executor: ((A | Promise[A]) -> unsafe.undefined, Any -> unsafe.undefined) -> unsafe.undefined) { +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── 'escapedCallback' is exposed by this compilation unit. //│ ║ in Promises.mls:449 //│ ║ l.449: val escapedCallback = @annotations.async {value => value.answer} @@ -478,7 +478,7 @@ noFulfillment().then(value => value.answer) :!js :fixme class Thenable with - fun id"then"(resolve: {answer: Int} -> undefined, reject: Any -> undefined) = resolve({answer: 77}) + fun id"then"(resolve: {answer: Int} -> unsafe.undefined, reject: Any -> unsafe.undefined) = resolve({answer: 77}) Promise.resolve(new Thenable).then(value => value.answer) //│ ╔══[COMPILATION ERROR] Resolution error in selection //│ ║ l.482: Promise.resolve(new Thenable).then(value => value.answer) diff --git a/hkmc2/shared/src/test/mlscript/newres/RecursiveBooleanInterfaces.mls b/hkmc2/shared/src/test/mlscript/newres/RecursiveBooleanInterfaces.mls index 7dd508ecf0..93add847cf 100644 --- a/hkmc2/shared/src/test/mlscript/newres/RecursiveBooleanInterfaces.mls +++ b/hkmc2/shared/src/test/mlscript/newres/RecursiveBooleanInterfaces.mls @@ -51,15 +51,6 @@ type First = Second type Second = Third type Third = First //│ ╔══[COMPILATION ERROR] Resolution error in member reference -//│ ║ l.50: type First = Second -//│ ║ ^^^^^^ -//│ ╟── Unguarded recursive type alias 'First'. -//│ ║ l.52: type Third = First -//│ ║ ^^^^^ -//│ ╟── Recursive references must pass through a record, tuple, function, or nominal type. -//│ ║ l.50: type First = Second -//│ ╙── ^^^^^ -//│ ╔══[COMPILATION ERROR] Resolution error in member reference //│ ║ l.51: type Second = Third //│ ║ ^^^^^ //│ ╟── Unguarded recursive type alias 'Second'. @@ -69,6 +60,15 @@ type Third = First //│ ║ l.51: type Second = Third //│ ╙── ^^^^^^ //│ ╔══[COMPILATION ERROR] Resolution error in member reference +//│ ║ l.50: type First = Second +//│ ║ ^^^^^^ +//│ ╟── Unguarded recursive type alias 'First'. +//│ ║ l.52: type Third = First +//│ ║ ^^^^^ +//│ ╟── Recursive references must pass through a record, tuple, function, or nominal type. +//│ ║ l.50: type First = Second +//│ ╙── ^^^^^ +//│ ╔══[COMPILATION ERROR] Resolution error in member reference //│ ║ l.52: type Third = First //│ ║ ^^^^^ //│ ╟── Unguarded recursive type alias 'Third'. diff --git a/hkmc2/shared/src/test/mlscript/newres/RecursiveEquality.mls b/hkmc2/shared/src/test/mlscript/newres/RecursiveEquality.mls index e902800d78..a976919f44 100644 --- a/hkmc2/shared/src/test/mlscript/newres/RecursiveEquality.mls +++ b/hkmc2/shared/src/test/mlscript/newres/RecursiveEquality.mls @@ -8,11 +8,11 @@ fun deepEqual(a, b) = if a === b then true a is Array as a and b is Array as b and a.length === b.length then a.every((item, index, ...) => deepEqual(item, b.at(index))) - else a !== undefined and a !== null and b !== undefined and b !== null and + else a !== unsafe.undefined and a !== null and b !== unsafe.undefined and b !== null and let ac = a!constructor - ac !== undefined and ac === b!constructor and + ac !== unsafe.undefined and ac === b!constructor and let md = ac!(Predef.Symbols.definitionMetadata) - md !== undefined and + md !== unsafe.undefined and md.2.every((field, ...) => field !== null and deepEqual(a!(field), b!(field))) class Box(val value: Int) [deepEqual([[1], [2]], [[1], [2]]), deepEqual([[1]], [[2]]), deepEqual(Box(3), Box(3)), deepEqual(Box(3), Box(4))] diff --git a/hkmc2/shared/src/test/mlscript/newres/SealedClasses.mls b/hkmc2/shared/src/test/mlscript/newres/SealedClasses.mls index c615fb5e2a..55e39e635a 100644 --- a/hkmc2/shared/src/test/mlscript/newres/SealedClasses.mls +++ b/hkmc2/shared/src/test/mlscript/newres/SealedClasses.mls @@ -122,7 +122,7 @@ class InvalidNested extends SealedClasses.Nested.Closed // The prelude's universal superclass is explicitly open. :expect 5 -class ExplicitObject extends Object { fun value() = 5 } +class ExplicitObject extends Object() { fun value() = 5 } new ExplicitObject().value() //│ = 5 diff --git a/hkmc2/shared/src/test/mlscript/newres/SpreadCalls.mls b/hkmc2/shared/src/test/mlscript/newres/SpreadCalls.mls index 1ca835bc47..e9202638e7 100644 --- a/hkmc2/shared/src/test/mlscript/newres/SpreadCalls.mls +++ b/hkmc2/shared/src/test/mlscript/newres/SpreadCalls.mls @@ -100,7 +100,7 @@ four(...[1, 2], ...[3, 4], 5) //│ ╟── Function 'four' expected 4 arguments, but got 5 //│ ║ l.5: fun four(a, b, c, d) = [a, b, c, d] //│ ╙── ^^^^^^^^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'four' expected 4 arguments but got 5 +//│ = [1, 2, 3, 4] // Opaque values are left to the runtime spread operation to validate. :re @@ -196,7 +196,7 @@ four(...arr) //│ ╟── Function 'four' expected 4 arguments, but got 2 //│ ║ l.5: fun four(a, b, c, d) = [a, b, c, d] //│ ╙── ^^^^^^^^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'four' expected 4 arguments but got 2 +//│ ═══[RUNTIME ERROR] Error: Function 'four' expected at least 4 arguments but got 2 // Exact call arity anchors known suffixes and prefixes around opaque spreads. @@ -240,7 +240,15 @@ invokeUnknown(...opaque([() => 95])) //│ ╟── Type 'Any' is declared without a member interface. //│ ║ in ../decls/Prelude.mls //│ ║ l.2: declare type Any -//│ ╙── ^^^ +//│ ║ ^^^ +//│ ╟── Value of unknown shape cannot be called like a function. +//│ ╟── Parameter 'f' admits values of unknown shape. +//│ ║ in SpreadCalls.mls:232 +//│ ║ l.232: fun invokeUnknown(f) = f() +//│ ║ ^ +//│ ╟── 'invokeUnknown' is exposed by this compilation unit. +//│ ║ l.232: fun invokeUnknown(f) = f() +//│ ╙── ^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. // Unknown elements must not silently resolve against another known candidate. @@ -249,7 +257,7 @@ fun inspectUnknown(x) = x.value inspectUnknown(Box(96)) inspectUnknown(...opaque([Box(96)])) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.248: fun inspectUnknown(x) = x.value +//│ ║ l.256: fun inspectUnknown(x) = x.value //│ ║ ^^^^^^^ //│ ╟── Cannot resolve member 'value' of a value with unknown shape. //│ ║ l.173: fun opaque(values): Array[Any] = values @@ -260,11 +268,11 @@ inspectUnknown(...opaque([Box(96)])) //│ ║ ^^^ //│ ╟── Cannot resolve member 'value' of a value with unknown shape. //│ ╟── Parameter 'x' admits values of unknown shape. -//│ ║ in SpreadCalls.mls:248 -//│ ║ l.248: fun inspectUnknown(x) = x.value +//│ ║ in SpreadCalls.mls:256 +//│ ║ l.256: fun inspectUnknown(x) = x.value //│ ║ ^ //│ ╟── 'inspectUnknown' is exposed by this compilation unit. -//│ ║ l.248: fun inspectUnknown(x) = x.value +//│ ║ l.256: fun inspectUnknown(x) = x.value //│ ╙── ^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -272,19 +280,19 @@ inspectUnknown(...opaque([Box(96)])) :e four(...opaque([]), 1, 2, 3, 4, 5) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.273: four(...opaque([]), 1, 2, 3, 4, 5) +//│ ║ l.281: four(...opaque([]), 1, 2, 3, 4, 5) //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Function 'four' expected 4 arguments, but got at least 5 //│ ║ l.5: fun four(a, b, c, d) = [a, b, c, d] //│ ╙── ^^^^^^^^^^^^^^^^^^^^ -//│ ═══[RUNTIME ERROR] Error: Function 'four' expected 4 arguments but got 5 +//│ = [1, 2, 3, 4] // Rest arity may be valid while the callable argument remains unknown. :e fun invokeRest(f, x, ...tail) = f() invokeRest(...opaque([() => 97]), Box(0)) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.284: fun invokeRest(f, x, ...tail) = f() +//│ ║ l.292: fun invokeRest(f, x, ...tail) = f() //│ ║ ^^^ //│ ╟── Value with no known interface cannot be called like a function. //│ ║ l.173: fun opaque(values): Array[Any] = values @@ -292,7 +300,15 @@ invokeRest(...opaque([() => 97]), Box(0)) //│ ╟── Type 'Any' is declared without a member interface. //│ ║ in ../decls/Prelude.mls //│ ║ l.2: declare type Any -//│ ╙── ^^^ +//│ ║ ^^^ +//│ ╟── Value of unknown shape cannot be called like a function. +//│ ╟── Parameter 'f' admits values of unknown shape. +//│ ║ in SpreadCalls.mls:292 +//│ ║ l.292: fun invokeRest(f, x, ...tail) = f() +//│ ║ ^ +//│ ╟── 'invokeRest' is exposed by this compilation unit. +//│ ║ l.292: fun invokeRest(f, x, ...tail) = f() +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. // When repeatedTuples receives [Box(98)], both a and b must acquire its element @@ -326,16 +342,14 @@ invokeTypedRest(...callbacks, Box(0)) fun invokePublicRest(f, x, ...tail) = f() invokePublicRest(...callbacks, Box(0)) //│ ╔══[COMPILATION ERROR] Resolution error in application -//│ ║ l.326: fun invokePublicRest(f, x, ...tail) = f() +//│ ║ l.342: fun invokePublicRest(f, x, ...tail) = f() //│ ║ ^^^ //│ ╟── Value of unknown shape cannot be called like a function. -//│ ║ l.326: fun invokePublicRest(f, x, ...tail) = f() -//│ ║ ^ //│ ╟── Parameter 'f' admits values of unknown shape. -//│ ║ l.326: fun invokePublicRest(f, x, ...tail) = f() +//│ ║ l.342: fun invokePublicRest(f, x, ...tail) = f() //│ ║ ^ //│ ╟── 'invokePublicRest' is exposed by this compilation unit. -//│ ║ l.326: fun invokePublicRest(f, x, ...tail) = f() +//│ ║ l.342: fun invokePublicRest(f, x, ...tail) = f() //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -371,10 +385,10 @@ class Child extends Base with fun readBase(xs: Array[Base]) = first(...xs).extra readBase([new Child]) //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.371: fun readBase(xs: Array[Base]) = first(...xs).extra +//│ ║ l.385: fun readBase(xs: Array[Base]) = first(...xs).extra //│ ║ ^^^^^^^^^^^^^^^^^^ //│ ╟── Value of type 'Base' does not contain member 'extra' -//│ ║ l.371: fun readBase(xs: Array[Base]) = first(...xs).extra +//│ ║ l.385: fun readBase(xs: Array[Base]) = first(...xs).extra //│ ╙── ^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. @@ -412,16 +426,16 @@ invokeUnannotatedArray(...unknownCallbacks) private fun firstInArray(xs) = first(...xs) [firstInArray(lefts).left, firstInArray(rights).right] //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.413: [firstInArray(lefts).left, firstInArray(rights).right] +//│ ║ l.427: [firstInArray(lefts).left, firstInArray(rights).right] //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Value of type 'Right' does not contain member 'left' -//│ ║ l.392: let rights = new mut Array[Right](0) +//│ ║ l.406: let rights = new mut Array[Right](0) //│ ╙── ^^^^^ //│ ╔══[COMPILATION ERROR] Resolution error in selection -//│ ║ l.413: [firstInArray(lefts).left, firstInArray(rights).right] +//│ ║ l.427: [firstInArray(lefts).left, firstInArray(rights).right] //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Value of type 'Left' does not contain member 'right' -//│ ║ l.391: let lefts = new mut Array[Left](0) +//│ ║ l.405: let lefts = new mut Array[Left](0) //│ ╙── ^^^^ //│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. //│ ═══[RUNTIME ERROR] Expected: '[43, 44]', got: 'undefined' diff --git a/hkmc2/shared/src/test/mlscript/newres/TuplePatternBindings.mls b/hkmc2/shared/src/test/mlscript/newres/TuplePatternBindings.mls index 63fb3ef1bd..7a6ab9bc90 100644 --- a/hkmc2/shared/src/test/mlscript/newres/TuplePatternBindings.mls +++ b/hkmc2/shared/src/test/mlscript/newres/TuplePatternBindings.mls @@ -59,3 +59,37 @@ fun classify(value) = else 3 [classify([1]), classify([0]), classify([-1]), classify(0), classify(1), classify([]), classify([1, 2])] //│ = [1, 3, 3, 2, 3, 3, 3] + +// Consuming a leading element can consume the fixed suffix when the spread is empty. +:expect 0 +private fun zero() = 0 +fun dropThenCall(xs: Array[Int]) = if [...xs, 42] is [_, ...rest] then zero(...rest) +dropThenCall([]) +//│ = 0 + +// A successful two-ended pattern needs a value after the fixed head. +class Head(val head: Int) +class Tail(val tail: Int) + +:expect 7 +fun trailing(xs: Array[Tail]) = if [Head(0), ...xs] is [_, ...middle, last] then last.tail +trailing([Tail(7)]) +//│ = 7 + +// Match the same case from the other end. +:expect 8 +fun leading(xs: Array[Head]) = if [...xs, Tail(0)] is [first, ...middle, _] then first.head +leading([Head(8)]) +//│ = 8 + +// The remainder can also be empty after removing a trailing element. +:expect 0 +fun dropLastThenCall(xs: Array[Int]) = if [42, ...xs] is [...rest, _] then zero(...rest) +dropLastThenCall([]) +//│ = 0 + +// Widening a possibly consumed field's length must retain its element flow. +:expect [2, 3] +fun restLengths(xs: Array[Str]) = if [...xs, "end"] is [_, ...rest] then rest.map(text => text.length) +restLengths(["a", "bb"]) +//│ = [2, 3] diff --git a/hkmc2/shared/src/test/mlscript/newres/TypeGraphTermination.mls b/hkmc2/shared/src/test/mlscript/newres/TypeGraphTermination.mls index 9ee2019aa9..fabc9b1df2 100644 --- a/hkmc2/shared/src/test/mlscript/newres/TypeGraphTermination.mls +++ b/hkmc2/shared/src/test/mlscript/newres/TypeGraphTermination.mls @@ -408,3 +408,56 @@ private fun walk[A](chain: Chain[A], n: Int) = type Chain[A, B] = {value: A, next: Chain[in B out A, in A out B]} private fun walk[A, B](chain: Chain[A, B], n: Int) = if n > 0 then walk(chain.next, n - 1) else () + + +// Each record points to the preceding alias twice. The source graph is linear +// in the number of aliases, even though its tree expansion has 2^24 leaves. +// Validation must visit shared interpretations once across all annotations. +:!js +type Level0 = {value: Int} +type Level1 = {left: Level0, right: Level0} +type Level2 = {left: Level1, right: Level1} +type Level3 = {left: Level2, right: Level2} +type Level4 = {left: Level3, right: Level3} +type Level5 = {left: Level4, right: Level4} +type Level6 = {left: Level5, right: Level5} +type Level7 = {left: Level6, right: Level6} +type Level8 = {left: Level7, right: Level7} +type Level9 = {left: Level8, right: Level8} +type Level10 = {left: Level9, right: Level9} +type Level11 = {left: Level10, right: Level10} +type Level12 = {left: Level11, right: Level11} +type Level13 = {left: Level12, right: Level12} +type Level14 = {left: Level13, right: Level13} +type Level15 = {left: Level14, right: Level14} +type Level16 = {left: Level15, right: Level15} +type Level17 = {left: Level16, right: Level16} +type Level18 = {left: Level17, right: Level17} +type Level19 = {left: Level18, right: Level18} +type Level20 = {left: Level19, right: Level19} +type Level21 = {left: Level20, right: Level20} +type Level22 = {left: Level21, right: Level21} +type Level23 = {left: Level22, right: Level22} +type Level24 = {left: Level23, right: Level23} +private fun unused(value: Level24) = 0 + + +// FIXME: Resolving an unused parameter annotation should not enumerate every +// combination of its union alternatives. TypeFormula.intersection distributes +// intersections over unions, so the n independent pairs below produce 2^n +// clauses. TypeFormula.normalized then compares clauses pairwise for absorption. +// All field names are distinct, so none of these clauses absorbs another. +// With 14 pairs this produces 16,384 clauses; the diff test took about 19 seconds +// during the resolver audit, even though the function never reads its parameter. +// Larger annotations become impractical without doing any useful resolution work. +// +// Keep the triggering code commented out so ordinary test runs stay fast. +// Uncomment the whole definition to reproduce the growth. A fix must avoid this +// eager expansion while retaining the stable identities used to stop recursive +// type substitutions from growing forever. It must also retain every possible +// member and argument flow; discarding alternatives to cap the size is incorrect. +// fun exponential(value: ({a0: Int} | {b0: Int}) & ({a1: Int} | {b1: Int}) & ({a2: Int} | {b2: Int}) & ({a3: Int} | {b3: Int}) & ({a4: Int} | {b4: Int}) & ({a5: Int} | {b5: Int}) & ({a6: Int} | {b6: Int}) & ({a7: Int} | {b7: Int}) & ({a8: Int} | {b8: Int}) & ({a9: Int} | {b9: Int}) & ({a10: Int} | {b10: Int}) & ({a11: Int} | {b11: Int}) & ({a12: Int} | {b12: Int}) & ({a13: Int} | {b13: Int})) = 0 + +// Exercise the same form at a size that is cheap enough for ordinary test runs. +:!js +fun small(value: ({a0: Int} | {b0: Int}) & ({a1: Int} | {b1: Int}) & ({a2: Int} | {b2: Int})) = 0 diff --git a/hkmc2/shared/src/test/mlscript/newres/TypeInterfaceDiagnostics.mls b/hkmc2/shared/src/test/mlscript/newres/TypeInterfaceDiagnostics.mls index a04ffc96bc..c2aa8cc643 100644 --- a/hkmc2/shared/src/test/mlscript/newres/TypeInterfaceDiagnostics.mls +++ b/hkmc2/shared/src/test/mlscript/newres/TypeInterfaceDiagnostics.mls @@ -292,3 +292,24 @@ fun inspect(value: Alias) = value.member //│ ╟── Type 'Hidden' is declared without a member interface. //│ ║ l.283: declare type Hidden //│ ╙── ^^^^^^ + +// Shared Any/Nothing nodes must not share the missing bound's diagnostic location. +:ge +fun firstInputOnly(xs: Array[in Int]) = xs.[0].length +fun secondInputOnly(xs: Array[in Str]) = xs.[0].length +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.298: fun firstInputOnly(xs: Array[in Int]) = xs.[0].length +//│ ║ ^^^^^^^^^^^ +//│ ╟── Cannot resolve member 'length' of a value with unknown shape. +//│ ╟── This type argument specifies only an input bound. +//│ ║ l.298: fun firstInputOnly(xs: Array[in Int]) = xs.[0].length +//│ ║ ^^^^^^ +//│ ╙── Values read through it have no output bound more specific than 'Any'. +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.299: fun secondInputOnly(xs: Array[in Str]) = xs.[0].length +//│ ║ ^^^^^^^^^^^ +//│ ╟── Cannot resolve member 'length' of a value with unknown shape. +//│ ╟── This type argument specifies only an input bound. +//│ ║ l.299: fun secondInputOnly(xs: Array[in Str]) = xs.[0].length +//│ ║ ^^^^^^ +//│ ╙── Values read through it have no output bound more specific than 'Any'. diff --git a/hkmc2/shared/src/test/mlscript/newres/TypeResolution.mls b/hkmc2/shared/src/test/mlscript/newres/TypeResolution.mls index 984f6e1f31..bfbe968907 100644 --- a/hkmc2/shared/src/test/mlscript/newres/TypeResolution.mls +++ b/hkmc2/shared/src/test/mlscript/newres/TypeResolution.mls @@ -33,10 +33,10 @@ fun ambiguous(n: Ambiguous) = n //│ ║ l.28: fun ambiguous(n: Ambiguous) = n //│ ║ ^^^^^^^^^ //│ ╟── candidate: type alias -//│ ║ l.27: type Ambiguous = Str +//│ ║ l.25: type Ambiguous = Int //│ ║ ^^^^^^^^^^^^^^ //│ ╟── candidate: type alias -//│ ║ l.25: type Ambiguous = Int +//│ ║ l.27: type Ambiguous = Str //│ ╙── ^^^^^^^^^^^^^^ // A type annotation selects the class even when its name also has a value. diff --git a/hkmc2/shared/src/test/mlscript/opt/DeadConstructorElim.mls b/hkmc2/shared/src/test/mlscript/opt/DeadConstructorElim.mls index af733b2433..a87aec4d1d 100644 --- a/hkmc2/shared/src/test/mlscript/opt/DeadConstructorElim.mls +++ b/hkmc2/shared/src/test/mlscript/opt/DeadConstructorElim.mls @@ -334,7 +334,7 @@ fun t(y) = 2 t(new O) //│ dead-constructor-elim > >>> dead-constructor-elim results >>> -//│ dead-constructor-elim > eliminable: Tuple(false, [Select(y¹, Ident(z)), 1, Select(y¹, Ident(x)), DynSelect(y¹, "z", false)]) +//│ dead-constructor-elim > eliminable: Tuple(false, [Select(y¹, Ident(z)), 1, Select(y¹, Ident(x)), DynSelect(y¹, "z", Unknown)]) //│ dead-constructor-elim > <<< dead-constructor-elim results <<< //│ ——————————————| Optimized IR |—————————————————————————————————————————————————————————————————————— //│ let tmp; diff --git a/hkmc2/shared/src/test/mlscript/opt/IllFormedAccesses.mls b/hkmc2/shared/src/test/mlscript/opt/IllFormedAccesses.mls index 5a99ab0024..432acb1afd 100644 --- a/hkmc2/shared/src/test/mlscript/opt/IllFormedAccesses.mls +++ b/hkmc2/shared/src/test/mlscript/opt/IllFormedAccesses.mls @@ -39,7 +39,7 @@ let x in :soir -let x = undefined in +let x = unsafe.undefined in if hide(false) do x = 1 print("don't inline cont.") diff --git a/hkmc2/shared/src/test/mlscript/slop/.mls b/hkmc2/shared/src/test/mlscript/slop/.mls new file mode 100644 index 0000000000..0954d5f30b --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/.mls @@ -0,0 +1,2 @@ +:.. +:js diff --git a/hkmc2/shared/src/test/mlscript/slop/ConstructorArity.mls b/hkmc2/shared/src/test/mlscript/slop/ConstructorArity.mls new file mode 100644 index 0000000000..dae055007c --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/ConstructorArity.mls @@ -0,0 +1,300 @@ +:. + + +// `new` checks the class constructor, independently of a function companion. +:e +fun Stack[A](...xs: Array[A]) = new Stack(1,2,3) +class Stack[out A] +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.6: fun Stack[A](...xs: Array[A]) = new Stack(1,2,3) +//│ ║ ^^^^^^^^^^^^^^^^ +//│ ╟── Class value expected 0 arguments, but got 3 +//│ ║ l.7: class Stack[out A] +//│ ╙── ^^^^^^^^^^^^^^^^^ + + +class Empty { + fun answer() = 42 +} + +:expect [42, 42] +[(new Empty).answer(), new Empty().answer()] +//│ = [42, 42] + +:e +fun extraArgument() = new Empty(1) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.25: fun extraArgument() = new Empty(1) +//│ ║ ^^^^^^^^^^^^ +//│ ╟── Class value expected 0 arguments, but got 1 +//│ ║ l.16: class Empty { +//│ ╙── ^^^^^^^^^^^ + +:e +fun extraEmptyList() = new Empty()() +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.34: fun extraEmptyList() = new Empty()() +//│ ║ ^^^^^^^^^^^^^ +//│ ╟── Class 'Empty' expected at most 1 argument list, but got 2 +//│ ║ l.16: class Empty { +//│ ╙── ^^^^^^^^^^^ + +// Spreads use the same tuple arity checking as ordinary function applications. +:e +fun nonemptySpread() = new Empty(...[1, 2]) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.44: fun nonemptySpread() = new Empty(...[1, 2]) +//│ ║ ^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Class value expected 0 arguments, but got 2 +//│ ║ l.16: class Empty { +//│ ╙── ^^^^^^^^^^^ + +:expect 42 +new Empty(...[]).answer() +//│ = 42 + +// An unknown spread length is accepted unless known elements exceed the arity. +fun uncertainSpread(xs: Array[Int]) = new Empty(...xs) + +:expect 42 +uncertainSpread([]).answer() +//│ = 42 + +:e +fun knownExtraWithSpread(xs: Array[Int]) = new Empty(1, ...xs) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.64: fun knownExtraWithSpread(xs: Array[Int]) = new Empty(1, ...xs) +//│ ║ ^^^^^^^^^^^^^^^^^^^ +//│ ╟── Class value expected 0 arguments, but got at least 1 +//│ ║ l.16: class Empty { +//│ ╙── ^^^^^^^^^^^ + + +// Qualified and aliased classes must retain their constructor's arity. +module Classes { class Empty } +val Alias = Classes.Empty +//│ Alias = class Empty + +:e +fun qualified() = new Classes.Empty(1) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.79: fun qualified() = new Classes.Empty(1) +//│ ║ ^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Class value expected 0 arguments, but got 1 +//│ ║ l.74: module Classes { class Empty } +//│ ╙── ^^^^^^^^^^^ + +:e +fun aliased() = new Alias(1) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.88: fun aliased() = new Alias(1) +//│ ║ ^^^^^^^^^^^^ +//│ ╟── Class value expected 0 arguments, but got 1 +//│ ║ l.74: module Classes { class Empty } +//│ ╙── ^^^^^^^^^^^ + + +class Pair(val first)(val second) { + fun values() = [first, second] +} + +// Partial construction remains callable until all parameter lists are supplied. +:expect [1, 2] +val partial = new Pair(1) +partial(2).values() +//│ = [1, 2] +//│ partial = fun + +:expect [3, 4] +new Pair(3)(4).values() +//│ = [3, 4] + +:e +fun tooManyLists() = new Pair(1)(2)(3) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.113: fun tooManyLists() = new Pair(1)(2)(3) +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Class 'Pair' expected at most 2 argument lists, but got 3 +//│ ║ l.97: class Pair(val first)(val second) { +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:e +fun tooManyArguments() = new Pair(1, 2)(3) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.122: fun tooManyArguments() = new Pair(1, 2)(3) +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Constructor of class 'Pair' expected 1 argument, but got 2 +//│ ║ l.97: class Pair(val first)(val second) { +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + +// Auxiliary constructor lists count even when absent from the class header. +class Auxiliary { + constructor(val first) + constructor(val second) + fun values() = [first, second] +} + +:expect [5, 6] +new Auxiliary(5)(6).values() +//│ = [5, 6] + +:e +fun extraAuxiliaryList() = new Auxiliary(5)(6)() +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.143: fun extraAuxiliaryList() = new Auxiliary(5)(6)() +//│ ║ ^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Class 'Auxiliary' expected at most 2 argument lists, but got 3 +//│ ║ l.132: class Auxiliary { +//│ ║ ^^^^^^^^^^^^^^^ +//│ ╟── The constructor is declared here. +//│ ║ l.133: constructor(val first) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^ +//│ ║ l.134: constructor(val second) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^ + + +// Refinements forward their written arguments to the parent constructor. +:expect [7, 8] +(new Pair(7)(8) with {}).values() +//│ = [7, 8] + +:e +fun invalidRefinement() = (new Empty(1) with {}) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.163: fun invalidRefinement() = (new Empty(1) with {}) +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Class value expected 0 arguments, but got 1 +//│ ║ l.16: class Empty { +//│ ╙── ^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Too many parameter lists for parent class +//│ ║ l.163: fun invalidRefinement() = (new Empty(1) with {}) +//│ ╙── ^^^^^^^^^^^^^^^^^ + +:e +class InvalidParent extends Empty(1) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.175: class InvalidParent extends Empty(1) +//│ ║ ^^^^^^^^ +//│ ╟── Class value expected 0 arguments, but got 1 +//│ ║ l.16: class Empty { +//│ ╙── ^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Too many parameter lists for parent class +//│ ║ l.175: class InvalidParent extends Empty(1) +//│ ╙── ^^^^^^^^ + + +// Foreign constructors use their declared parameter lists. Their optional slots +// preserve undefined so native defaults run before any values reach MLscript code. +:expect ["NaN", "NaN", "NaN"] +val NativeNumber = Number.class +[String(new Number()), String(new Number(unsafe.undefined)), String(new NativeNumber())] +//│ = ["NaN", "NaN", "NaN"] +//│ NativeNumber = class Number + +:noInline +:expect ["NaN", "NaN", "NaN", "NaN"] +:w +val makeNumber = new Number +val makeNativeNumber = new NativeNumber +[String(makeNumber()), String(makeNumber(unsafe.undefined)), + String(makeNativeNumber()), String(makeNativeNumber(unsafe.undefined))] +//│ ╔══[WARNING] This 'new' expression partially applies class 'Number' instead of constructing an instance. +//│ ║ l.198: val makeNumber = new Number +//│ ║ ^^^^^^^^^^ +//│ ╟── Use `new Number()` to construct an instance, or `=> new Number()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ in ../decls/Prelude.mls +//│ ║ l.32: constructor(value?) +//│ ╙── ^^^^^^ +//│ ╔══[WARNING] This 'new' expression partially applies class 'Number' instead of constructing an instance. +//│ ║ l.199: val makeNativeNumber = new NativeNumber +//│ ║ ^^^^^^^^^^^^^^^^ +//│ ╟── Use `new NativeNumber()` to construct an instance, or `=> new NativeNumber()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ in ../decls/Prelude.mls +//│ ║ l.32: constructor(value?) +//│ ╙── ^^^^^^ +//│ = ["NaN", "NaN", "NaN", "NaN"] +//│ makeNativeNumber = fun +//│ makeNumber = fun + +:expect "message" +new Error("message").message +//│ = "message" + +:expect ["", "message", "message"] +[new Error().message, new Error("message", {}).message, new TypeError("message").message] +//│ = ["", "message", "message"] + +:e +fun extraErrorArgument() = new Error("message", {}, 3) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.231: fun extraErrorArgument() = new Error("message", {}, 3) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Constructor of class 'Error' expected 0 to 2 arguments, but got 3 +//│ ║ in ../decls/Prelude.mls +//│ ║ l.130: declare class Error { +//│ ║ ^^^^^^^^^^^^^^^^^^^ +//│ ╟── The constructor is declared here. +//│ ║ l.131: constructor(message?, options?) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +// Foreign classes without a constructor list do not accept arbitrary arguments. +declare class NoConstructor + +:e +fun undeclaredConstructor() = new NoConstructor(1) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.247: fun undeclaredConstructor() = new NoConstructor(1) +//│ ║ ^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Class value expected 0 arguments, but got 1 +//│ ║ l.244: declare class NoConstructor +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +// Written foreign constructor parameter lists are still checked. +declare class String with + constructor(value: Str) + val length: Int + +:expect 5 +new String("hello").length +//│ = 5 + +:e +fun extraForeignArgument() = new String("hello", "world") +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.265: fun extraForeignArgument() = new String("hello", "world") +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Constructor of class 'String' expected 1 argument, but got 2 +//│ ║ l.256: declare class String with +//│ ║ ^^^^^^^^^^^^^^^^^^^^ +//│ ╟── The constructor is declared here. +//│ ║ l.257: constructor(value: Str) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^ + +:e +fun extraForeignList() = new String("hello")() +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.277: fun extraForeignList() = new String("hello")() +//│ ║ ^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Class 'String' expected at most 1 argument list, but got 2 +//│ ║ l.256: declare class String with +//│ ║ ^^^^^^^^^^^^^^^^^^^^ +//│ ╟── The constructor is declared here. +//│ ║ l.257: constructor(value: Str) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^ + + +// Obvious constructor arity mistakes are also errors in lenient resolution. +#lang(strictResolution: false, eagerResolution: false) + +:e +fun LooseStack[A](...xs: Array[A]) = new LooseStack(1,2,3) +class LooseStack[out A] +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.293: fun LooseStack[A](...xs: Array[A]) = new LooseStack(1,2,3) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Class value expected 0 arguments, but got 3 +//│ ║ l.294: class LooseStack[out A] +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/slop/JsSymbol.mls b/hkmc2/shared/src/test/mlscript/slop/JsSymbol.mls new file mode 100644 index 0000000000..ec1cce4797 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/JsSymbol.mls @@ -0,0 +1,584 @@ +:. + +open annotations { fresh, jsSymbol, noInline } + +// The key's declared type is intentionally Any: the resolver does not type-check it. +module Keys { + @fresh val first: Any = Symbol("first") + @fresh val second = Symbol("second") + @fresh val strKey = "hello" +} + +:expect [41, 42, 42, 1337] +open class Base { + @jsSymbol(Keys.first) open fun read() = 41 + @jsSymbol(Keys.strKey) open fun hi = 1337 +} +class Child extends Base { + @jsSymbol(Keys.first) override fun read() = super.read() + 1 +} +let c = new Child, [ + new Base().read() + c.read() + c.(Keys.first)() + c.(Keys.strKey) +] +//│ = [41, 42, 42, 1337] +//│ c = Child + +:expect [1337, 1337] +[c.hi, c.'hello] +//│ = [1337, 1337] + +:e +c.hello +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.34: c.hello +//│ ║ ^^^^^^^ +//│ ╟── Instance of class 'Child' does not contain member 'hello' +//│ ║ l.20: let c = new Child, [ +//│ ╙── ^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// Structural signatures and computed record fields use the same key identity. +:expect [43, 44] +type Reader = {@jsSymbol(Keys.second) read: () -> Int} +private fun read(value: Reader) = value.read() +[read({(Keys.second): () => 43}), read({(Keys.second): () => 44})] +//│ = [43, 44] + +// An immutable value may occur in the middle of a stable path. +:expect 45 +class KeyHolder { + @fresh val key = Symbol("held") +} +val holder = new KeyHolder +class Held { + @jsSymbol(holder.key) fun read() = 45 +} +(new Held).read() +//│ = 45 +//│ holder = KeyHolder { key: Symbol("held") } + +// Local keys must survive substitution, and are evaluated separately on each call. +:expect [46, 47] +private fun local(value) = + @fresh val key = Symbol("local") + class Local { + @jsSymbol(key) fun read() = value + } + (new Local).read() +[local(46), local(47)] +//│ = [46, 47] + +// The same property key cannot be overridden under a different source name. +:e +class Renamed extends Base { + @jsSymbol(Keys.first) override fun other() = 0 +} +//│ ╔══[COMPILATION ERROR] Member 'other' uses the same @jsSymbol key as inherited member 'read'; overrides must keep the inherited name. +//│ ║ l.77: @jsSymbol(Keys.first) override fun other() = 0 +//│ ║ ^^^^^ +//│ ╟── The inherited member is declared here. +//│ ║ l.14: @jsSymbol(Keys.first) open fun read() = 41 +//│ ╙── ^^^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] Member 'other' is marked 'override' but overrides no inherited member +//│ ║ l.77: @jsSymbol(Keys.first) override fun other() = 0 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:e +class DifferentKey extends Base { + @jsSymbol(Keys.second) override fun read() = 0 +} +//│ ╔══[COMPILATION ERROR] Overriding member 'read' must use the same @jsSymbol annotation as its parent. +//│ ║ l.91: @jsSymbol(Keys.second) override fun read() = 0 +//│ ║ ^^^^ +//│ ╟── The inherited member is declared here. +//│ ║ l.14: @jsSymbol(Keys.first) open fun read() = 41 +//│ ╙── ^^^^ + +// An override must keep the runtime key declared by its parent. +:e +class MissingSymbol extends Base { + override fun read() = 0 +} +//│ ╔══[COMPILATION ERROR] Overriding member 'read' must use the same @jsSymbol annotation as its parent. +//│ ║ l.103: override fun read() = 0 +//│ ║ ^^^^ +//│ ╟── The inherited member is declared here. +//│ ║ l.14: @jsSymbol(Keys.first) open fun read() = 41 +//│ ╙── ^^^^ + +:e +class Duplicate { + @jsSymbol(Keys.first) fun read() = 0 + @jsSymbol(Keys.first) fun other() = 1 +} +//│ ╔══[COMPILATION ERROR] Duplicate definition of member 'Keys.first'. +//│ ╟── Member 'read' is defined here. +//│ ║ l.114: @jsSymbol(Keys.first) fun read() = 0 +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Member 'other' is defined here. +//│ ║ l.115: @jsSymbol(Keys.first) fun other() = 1 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +val ordinary = Symbol("ordinary") +//│ ordinary = Symbol("ordinary") + +:e +class Unmarked { + @jsSymbol(ordinary) fun read() = 0 +} +//│ ╔══[COMPILATION ERROR] Resolution error in member reference +//│ ║ l.130: @jsSymbol(ordinary) fun read() = 0 +//│ ║ ^^^^^^^^ +//│ ╟── @jsSymbol requires a val marked @annotations.fresh. +//│ ║ l.130: @jsSymbol(ordinary) fun read() = 0 +//│ ║ ^^^^^^^^ +//│ ╟── The referenced member is declared here. +//│ ║ l.125: val ordinary = Symbol("ordinary") +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:e +class Literal { + @jsSymbol("iterator") fun read() = 0 +} +//│ ╔══[COMPILATION ERROR] Resolution error in string literal +//│ ║ l.144: @jsSymbol("iterator") fun read() = 0 +//│ ║ ^^^^^^^^^^ +//│ ╟── @jsSymbol requires a stable path of modules and immutable vals ending in a val marked @annotations.fresh. +//│ ║ l.144: @jsSymbol("iterator") fun read() = 0 +//│ ╙── ^^^^^^^^^^ + +:e +class Call { + @jsSymbol(Symbol("called")) fun read() = 0 +} +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.155: @jsSymbol(Symbol("called")) fun read() = 0 +//│ ║ ^^^^^^^^^^^^^^^^ +//│ ╟── @jsSymbol requires a stable path of modules and immutable vals ending in a val marked @annotations.fresh. +//│ ║ l.155: @jsSymbol(Symbol("called")) fun read() = 0 +//│ ╙── ^^^^^^^^^^^^^^^^ + +let changing = holder +//│ changing = KeyHolder { key: Symbol("held") } + +:e +class MutablePrefix { + @jsSymbol(changing.key) fun read() = 0 +} +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.169: @jsSymbol(changing.key) fun read() = 0 +//│ ║ ^^^^^^^^^^^^ +//│ ╟── @jsSymbol requires a stable path of modules and immutable vals ending in a val marked @annotations.fresh. +//│ ║ l.169: @jsSymbol(changing.key) fun read() = 0 +//│ ╙── ^^^^^^^^^^^^ + +// The annotation is a prelude declaration, usable through ordinary opens. +:expect [50, 51] +private fun opened() = + open annotations { jsSymbol } + class Opened { + @jsSymbol(Keys.first) fun read() = 50 + } + (new Opened).read() +private fun wildcard() = + open annotations + class Wildcard { + @jsSymbol(Keys.second) fun read() = 51 + } + (new Wildcard).read() +[opened(), wildcard()] +//│ = [50, 51] + +// A user function with the same spelling does not change a method's key. +:w +:expect 52 +private fun shadowed() = + fun jsSymbol(key) = key + class Ordinary { + @jsSymbol(Keys.first) fun read() = 52 + } + (new Ordinary).("read")() +shadowed() +//│ ╔══[WARNING] This annotation has no effect. +//│ ║ l.201: @jsSymbol(Keys.first) fun read() = 52 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^ +//│ = 52 + +// An escaping instance can't refer back to the local key. +// Hoisting the key would incorrectly share it between different invocations. +@noInline +private fun escaping(value) = + @fresh val localKey = Symbol("escaping") + class Escaped { + @jsSymbol(localKey) fun read() = value + } + new Escaped + +// Lowering must report the unavailable key without emitting an out-of-scope reference. +:sir +:e +escaping(54).read() +//│ ╔══[COMPILATION ERROR] Cannot use @jsSymbol key 'localKey' here: 'localKey' is not in scope. +//│ ║ l.223: escaping(54).read() +//│ ║ ^^^^^ +//│ ╟── The key's root binding is defined here. +//│ ║ l.214: @fresh val localKey = Symbol("escaping") +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ throw "This code cannot be run as its compilation yielded an error." +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// Imported keys must keep their defining module alive through IR transformations. +import "../../mlscript-compile/SymbolKeyLibrary.mls" + +:expect [55, 56] +[(new SymbolKeyLibrary.Reader).read(), SymbolKeyLibrary.make(56).read()] +//│ = [55, 56] + +// Direct immutable receiver aliases identify the same fresh field. +:expect 57 +val holderAlias = holder +open class HeldBase { + @jsSymbol(holder.key) open fun read() = 0 +} +class HeldChild extends HeldBase { + @jsSymbol(holderAlias.key) override fun read() = 57 +} +(new HeldChild).read() +//│ = 57 +//│ holderAlias = KeyHolder { key: Symbol("held") } + +// A selected receiver alias must replace its entire path, including its prefix. +:expect 58 +module Aliases { + val nested = holder +} +class SelectedAlias extends HeldBase { + @jsSymbol(Aliases.nested.key) override fun read() = 58 +} +(new SelectedAlias).read() +//│ = 58 + +// Reading and assigning through a structural alias use the same symbol property. +:expect 59 +type Cell = {@jsSymbol(Keys.first) value: Int} +private fun update(cell: Cell) = + set cell.value = 59 + cell.value +update(mut {(Keys.first): 0}) +//│ = 59 + +// A returned closure captures the original key, even when its name is shadowed. +:expect [60, 61] +@noInline +private fun captured(value) = + @fresh val capturedKey = Symbol("captured") + class Captured { + @jsSymbol(capturedKey) fun read() = value + } + val instance = new Captured + () => + @fresh val capturedKey = Symbol("shadow") + instance.read() +[captured(60)(), captured(61)()] +//│ = [60, 61] + +// Local modules are bindings too; a sibling scope cannot recover their keys. +@noInline +private fun moduleKey(value) = + module LocalKeys { @fresh val key = Symbol("module key") } + class Local { + @jsSymbol(LocalKeys.key) fun fetch() = value + } + new Local + +:e +:sir +moduleKey(62).fetch() +//│ ╔══[COMPILATION ERROR] Cannot use @jsSymbol key 'LocalKeys.key' here: 'LocalKeys' is not in scope. +//│ ║ l.302: moduleKey(62).fetch() +//│ ║ ^^^^^^ +//│ ╟── The key's root binding is defined here. +//│ ║ l.294: module LocalKeys { @fresh val key = Symbol("module key") } +//│ ╙── ^^^^^^^^^ +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ throw "This code cannot be run as its compilation yielded an error." +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// A structural field needs its key for both reading and writing. +@noInline +private fun localCell(value) = + @fresh val cellKey = Symbol("cell") + type Cell = {@jsSymbol(cellKey) value: Int} + val cell: Cell = mut {(cellKey): value} + cell + +:e +localCell(0).value +//│ ╔══[COMPILATION ERROR] Cannot use @jsSymbol key 'cellKey' here: 'cellKey' is not in scope. +//│ ║ l.323: localCell(0).value +//│ ║ ^^^^^^ +//│ ╟── The key's root binding is defined here. +//│ ║ l.317: @fresh val cellKey = Symbol("cell") +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +:e +set localCell(0).value = 63 +//│ ╔══[COMPILATION ERROR] Cannot use @jsSymbol key 'cellKey' here: 'cellKey' is not in scope. +//│ ║ l.333: set localCell(0).value = 63 +//│ ║ ^^^^^^ +//│ ╟── The key's root binding is defined here. +//│ ║ l.317: @fresh val cellKey = Symbol("cell") +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// Opened calls must use the same checked key lowering as explicit selections. +:e +private fun openedEscaped() = + val instance = moduleKey(0) + open instance + fetch() +openedEscaped() +//│ ╔══[COMPILATION ERROR] Cannot use @jsSymbol key 'LocalKeys.key' here: 'LocalKeys' is not in scope. +//│ ║ l.347: fetch() +//│ ║ ^^^^^ +//│ ╟── The key's root binding is defined here. +//│ ║ l.294: module LocalKeys { @fresh val key = Symbol("module key") } +//│ ╙── ^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +:expect 67 +private fun openedAvailable() = + @fresh val key = Symbol("opened") + class Opened { @jsSymbol(key) fun fetch() = 67 } + val instance = new Opened + open instance + fetch() +openedAvailable() +//│ = 67 + +// An imported file-level key has a real private export; factory locals do not. +:expect 64 +:sjs +new SymbolKeyLibrary.FileReader().read() +//│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— +//│ import { _$_modulePrivate_$_fileKey as fileKey } from "./../../mlscript-compile/SymbolKeyLibrary.mjs"; +//│ let tmp22; +//│ tmp22 = globalThis.Object.freeze(new SymbolKeyLibrary.FileReader()); +//│ return tmp22[fileKey]() +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = 64 + +// Private accessor symbols are local to the defining file. +:e +:sjs +new SymbolKeyLibrary.FileReader().readPriv() +//│ ╔══[COMPILATION ERROR] Cannot use @jsSymbol key 'SymbolKeyLibrary.privateKey' here: private member 'privateKey' belongs to another file. +//│ ║ l.382: new SymbolKeyLibrary.FileReader().readPriv() +//│ ║ ^^^^^^^^^ +//│ ╟── The private member is declared here. +//│ ║ in ../../mlscript-compile/SymbolKeyLibrary.mls +//│ ║ l.9: @fresh private val privateKey = Symbol("private key") +//│ ║ ^^^^^^^^^^ +//│ ╟── The keyed member 'readPriv' is defined here. +//│ ║ l.32: @jsSymbol(privateKey) fun readPriv() = 65 +//│ ╙── ^^^^^^^^ +//│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— +//│ throw "This code cannot be run as its compilation yielded an error." +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +:e +:sir +SymbolKeyLibrary.makeLocal(65).read() +//│ ╔══[COMPILATION ERROR] Cannot use @jsSymbol key 'key' here: 'key' is not in scope. +//│ ║ l.400: SymbolKeyLibrary.makeLocal(65).read() +//│ ║ ^^^^^ +//│ ╟── The key's root binding is defined here. +//│ ║ in ../../mlscript-compile/SymbolKeyLibrary.mls +//│ ║ l.35: @fresh val key = Symbol("factory key") +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ throw "This code cannot be run as its compilation yielded an error." +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// Inherited implementations also need the key at the super access site. +@noInline +private fun baseWithLocalKey() = + @fresh val baseKey = Symbol("base") + open class LocalBase { + @jsSymbol(baseKey) open fun read() = 66 + } + LocalBase.class +val EscapedBase = baseWithLocalKey() +//│ EscapedBase = class LocalBase + +:e +class MissingSuperKey extends EscapedBase { + fun run() = super.read() +} +(new MissingSuperKey).run() +//│ ╔══[COMPILATION ERROR] Cannot use @jsSymbol key 'baseKey' here: 'baseKey' is not in scope. +//│ ║ l.426: fun run() = super.read() +//│ ║ ^^^^^ +//│ ╟── The key's root binding is defined here. +//│ ║ l.416: @fresh val baseKey = Symbol("base") +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// The annotation's unqualified module reference needs its enclosing path at the caller. +:expect [68, 69] +module OuterKeys { + module Nested { + @fresh val key = Symbol("nested key") + class Reader { + @jsSymbol(key) fun read() = 69 + } + } +} +[new SymbolKeyLibrary.Nested.Reader().read(), new OuterKeys.Nested.Reader().read()] +//│ = [68, 69] + +// Sibling scopes may use a module defined in their common lexical parent. +:expect [70, 71] +@noInline +private fun siblingReaders(value) = + module Local { + @fresh val key = Symbol("sibling key") + class Reader { + @jsSymbol(key) fun read() = value + } + } + fun make() = new Local.Reader + fun read() = make().read() + read() +[siblingReaders(70), siblingReaders(71)] +//│ = [70, 71] + +// Different implementations of an inherited member retain the same key operand. +:expect [41, 42] +private fun inheritedRead(value) = value.read() +[inheritedRead(new Base), inheritedRead(new Child)] +//│ = [41, 42] + +// A private intermediate module is just as inaccessible across files as a private key val. +:e +:sir +new SymbolKeyLibrary.PrivatePathReader().read() +//│ ╔══[COMPILATION ERROR] Cannot use @jsSymbol key 'SymbolKeyLibrary.PrivateKeys.key' here: private member 'PrivateKeys' belongs to another file. +//│ ║ l.475: new SymbolKeyLibrary.PrivatePathReader().read() +//│ ║ ^^^^^ +//│ ╟── The private member is declared here. +//│ ║ in ../../mlscript-compile/SymbolKeyLibrary.mls +//│ ║ l.19: private module PrivateKeys { +//│ ║ ^^^^^^^^^^^ +//│ ╟── The keyed member 'read' is defined here. +//│ ║ l.23: @jsSymbol(PrivateKeys.key) fun read() = 72 +//│ ╙── ^^^^ +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ throw "This code cannot be run as its compilation yielded an error." +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// The same-file accessor remains available even outside the key's owner. +:expect 73 +module PrivateOwner { + @fresh private val key = Symbol("same-file private key") + class Reader { + @jsSymbol(key) fun read() = 73 + } +} +new PrivateOwner.Reader().read() +//│ = 73 + +// Both source aliases remain available when structural interfaces are combined. +:expect [18, 18] +type SourceOne = {@jsSymbol(Symbol.iterator) first: () -> Iterator[Int]} +type SourceTwo = {@jsSymbol(Symbol.iterator) second: () -> Iterator[Int]} +private fun both(xs: SourceOne & SourceTwo) = [xs.first().next().value, xs.second().next().value] +both([18]) +//│ = [18, 18] + +// A source alias and a string property with the same name are distinct targets. +:e +private fun collision(xs: SourceOne & {first: () -> Iterator[Str]}) = xs.first() +//│ ╔══[COMPILATION ERROR] This reference has targets with different property keys. +//│ ║ l.512: private fun collision(xs: SourceOne & {first: () -> Iterator[Str]}) = xs.first() +//│ ║ ^^^^^^ +//│ ╟── Candidate 'first' is declared here. +//│ ║ l.504: type SourceOne = {@jsSymbol(Symbol.iterator) first: () -> Iterator[Int]} +//│ ║ ^^^^^ +//│ ╟── Candidate 'first' is declared here. +//│ ║ l.512: private fun collision(xs: SourceOne & {first: () -> Iterator[Str]}) = xs.first() +//│ ╙── ^^^^^ + +// An unused annotation must not rename a field behind its source-level name. +:w +:expect [19, 19] +class Plain { + @jsSymbol(Symbol.iterator) val value = 19 +} +let plain = new Plain +[plain.value, plain.("value")] +//│ ╔══[WARNING] Only public methods may use @jsSymbol. +//│ ║ l.527: @jsSymbol(Symbol.iterator) val value = 19 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ = [19, 19] +//│ plain = Plain { value: 19 } + +@annotations.bufferable +class BufferedReader { + @jsSymbol(Keys.second) fun read(): Str = "abc" +} + +// FIXME: DefnShape.bufferedMembers invents a string-keyed field, but +// BufferableTransform emits the method under Keys.second. +:fixme +:re +:expect 3 +BufferedReader.read(null, 0)().length +//│ ═══[RUNTIME ERROR] TypeError: BufferedReader1.read is not a function +//│ ═══[RUNTIME ERROR] Expected: '3', got: 'undefined' + +// Under `:ftc`, function values are objects with a `call` method, so a keyed method used as a +// value must be eta-expanded like a method selected by name. +:ftc +:sir +:expect [74, 75] +class FtcKeyed { + @jsSymbol(Keys.second) fun next(x) = x + 1 +} +let ftcKeyed = new FtcKeyed +let ftcNext = ftcKeyed.next +[ftcKeyed.next(73), ftcNext(74)] +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ let tmp, tmp1, Function$, tmp2; +//│ define FtcKeyed⁰ as class FtcKeyed¹ { +//│ method next⁰ = fun next¹(x) { +//│ return +⁰(x, 1) +//│ } +//│ }; +//│ set ftcKeyed⁰ = new FtcKeyed¹(); +//│ define Function$ as class Function$⁰ extends globalThis⁰.Function⁰ { +//│ method call⁰ = fun call¹(x) { +//│ return ftcKeyed⁰.[Keys⁰.second⁰](x) +//│ } +//│ }; +//│ set tmp2 = new Function$⁰(); +//│ set ftcNext⁰ = tmp2; +//│ set tmp = ftcKeyed⁰.[Keys⁰.second⁰](73); +//│ set tmp1 = ftcNext⁰.call﹖(74); +//│ return [tmp, tmp1] +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— +//│ = [74, 75] +//│ ftcKeyed = FtcKeyed +//│ ftcNext = class anonymous + diff --git a/hkmc2/shared/src/test/mlscript/slop/OptionalBinding.mls b/hkmc2/shared/src/test/mlscript/slop/OptionalBinding.mls new file mode 100644 index 0000000000..431c31589e --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/OptionalBinding.mls @@ -0,0 +1,138 @@ +:. +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 new file mode 100644 index 0000000000..8207f6bf35 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/OptionalConstructors.mls @@ -0,0 +1,151 @@ +:. + + +class Main(val first, val second?: Int) { + fun values() = [first, second] +} + +:expect [[1, null], [2, 3], [4, null]] +[Main(1).values(), new Main(2, 3).values(), new Main(4, unsafe.undefined).values()] +//│ = [[1, null], [2, 3], [4, null]] + +// Partial construction preserves optionality through generated forwarding functions. +:noInline +:expect [[5, null], [6, 7]] +val makeMain = new Main +[makeMain(5).values(), makeMain(6, 7).values()] +//│ = [[5, null], [6, 7]] +//│ makeMain = fun + +class Auxiliary { + constructor(val first, val second?) + fun values() = [first, second] +} + +:expect [[8, null], [9, 10]] +[new Auxiliary(8).values(), new Auxiliary(9, 10).values()] +//│ = [[8, null], [9, 10]] + +class Curried(val first?)(val second?: Int) { + constructor(val third?) + fun values() = [first, second, third] +} + +:expect [[null, null, null], [11, 12, 13]] +[new Curried()()().values(), Curried(11)(12)(13).values()] +//│ = [[null, null, null], [11, 12, 13]] + +// Omitted arguments in earlier lists must not shift later arguments into their fields. +:expect [[null, 21, 22], [23, null, 24]] +[new Curried()(21)(22).values(), Curried(23)()(24).values()] +//│ = [[null, 21, 22], [23, null, 24]] + +fun spreadFirst(xs: Array[Int]) = new Curried(...xs)(25)(26) + +:expect [[null, 25, 26], [27, 25, 26]] +[spreadFirst([]).values(), spreadFirst([27]).values()] +//│ = [[null, 25, 26], [27, 25, 26]] + +:noInline +:expect [14, null, null] +val finish = new Curried(14) +finish()().values() +//│ = [14, null, null] +//│ finish = fun + +// Omission supplies null to the constructor body and to mutable fields. +class Mutable(mut val value?) { + val initiallyMissing = value is null +} + +:expect [true, 15] +val mutable = new mut Mutable() +set mutable.value = 15 +[mutable.initiallyMissing, mutable.value] +//│ = [true, 15] +//│ mutable = Mutable(15) + +data class Data(value?) + +:expect [null, 16] +[Data().value, Data(16).value] +//│ = [null, 16] + +// Constructor patterns still match every field, including omitted optional values. +:expect [true, false] +fun missing(x) = if x is Data(value) then value is null else false +[missing(Data()), missing(Data(17))] +//│ = [true, false] + +// Optional fields retain their null alternative in the declared interface. +class Item(val value) +class Generic[A](val item?: A) +fun read(x: Generic[Item]) = if x.item is Item as item then item.value else 0 + +:expect [0, 18] +[read(Generic[Item]()), read(Generic(Item(18)))] +//│ = [0, 18] + +// Parent constructors receive omitted arguments through ordinary constructor calls. +class Child extends Main(19) + +:expect [[19, null], [20, null]] +[(new Child).values(), (new Main(20) with {}).values()] +//│ = [[19, null], [20, null]] + +// Lifted local classes preserve captured values and materialized optional slots. +:lift +:expect [29, null] +fun local(captured) = { + class Local(val value?) { + fun values() = [captured, value] + } + new Local() +} +local(29).values() +//│ = [29, null] + +// Optionality does not remove required parameters or permit extra arguments. +:e +fun missingRequired() = new Main() +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.110: fun missingRequired() = new Main() +//│ ║ ^^^^^^^^^^ +//│ ╟── Constructor of class 'Main' expected 1 to 2 arguments, but got 0 +//│ ║ l.4: class Main(val first, val second?: Int) { +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:e +fun tooMany() = new Main(1, 2, 3) +//│ ╔══[COMPILATION ERROR] Resolution error in object instantiation +//│ ║ l.119: fun tooMany() = new Main(1, 2, 3) +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Constructor of class 'Main' expected 1 to 2 arguments, but got 3 +//│ ║ l.4: class Main(val first, val second?: Int) { +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:e +class BadOrder(first?, second) +//│ ╔══[COMPILATION ERROR] A required parameter cannot follow an optional parameter. +//│ ║ l.128: class BadOrder(first?, second) +//│ ╙── ^^^^^^ + +:e +class BadAuxiliary { + constructor(first?, second) +} +//│ ╔══[COMPILATION ERROR] A required parameter cannot follow an optional parameter. +//│ ║ l.135: constructor(first?, second) +//│ ╙── ^^^^^^ + +:e +class BadContext(using value?: Int) +//│ ╔══[COMPILATION ERROR] Contextual parameters cannot be optional. +//│ ║ l.142: class BadContext(using value?: Int) +//│ ╙── ^^^^^^^^^^^^^^^^^ + +:e +class BadDrop(drop value) +//│ ╔══[COMPILATION ERROR] Droppable parameters are only supported in function and lambda parameter lists. +//│ ║ l.148: class BadDrop(drop value) +//│ ╙── ^^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/slop/OptionalEtaExpansion.mls b/hkmc2/shared/src/test/mlscript/slop/OptionalEtaExpansion.mls new file mode 100644 index 0000000000..4ccd94ad4b --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/OptionalEtaExpansion.mls @@ -0,0 +1,35 @@ +:. +:flowBasedOpt +:noInline +open annotations + +// Both calls are valid; the returned functions need not agree on optionality. +// No function type annotations are involved. Affinity enables eta expansion. +:expect [1, 1] +@affine(0) private fun required(x) = x +@affine(0) private fun optional(x?) = if x is Int then x else 0 +private fun choose(b) = if b then required else optional +[choose(true)(1), choose(false)(1)] +//│ = [1, 1] + +// A fixed/rest disagreement also ends expansion without invalidating the calls. +:expect [2, 2] +@affine(0) private fun fixed(x) = x +@affine(0) private fun variadic(x, ...xs) = x + xs.length +private fun chooseRest(b) = if b then fixed else variadic +[chooseRest(true)(2), chooseRest(false)(2)] +//│ = [2, 2] + +// Keep the compatible prefix even if a later returned function differs. +:flowBasedOpt debugEta +:expect [12, 12] +@affine(0) private fun firstRequired(x) = @affine(0) y => x + y +@affine(0) private fun firstOptional(x) = @affine(0) (y?) => x + (if y is Int then y else 0) +private fun nested(b) = if b then firstRequired else firstOptional +[nested(true)(10)(2), nested(false)(10)(2)] +//│ eta-expansion > >>> eta-expansion targets shapes >>> +//│ eta-expansion > nested⁰#0 @ : [1, 1] +//│ eta-expansion > firstOptional⁰#0 @ : [1, 1(1 optional)] +//│ eta-expansion > firstRequired⁰#0 @ : [1, 1] +//│ eta-expansion > <<< eta-expansion targets shapes <<< +//│ = [12, 12] diff --git a/hkmc2/shared/src/test/mlscript/slop/PartialInstantiationWarnings.mls b/hkmc2/shared/src/test/mlscript/slop/PartialInstantiationWarnings.mls new file mode 100644 index 0000000000..31e194eece --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/PartialInstantiationWarnings.mls @@ -0,0 +1,159 @@ +:. + + +class Empty() + +:w +:expect true +val makeEmpty = new Empty +makeEmpty() is Empty +//│ ╔══[WARNING] This 'new' expression partially applies class 'Empty' instead of constructing an instance. +//│ ║ l.8: val makeEmpty = new Empty +//│ ║ ^^^^^^^^^ +//│ ╟── Use `new Empty()` to construct an instance, or `=> new Empty()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ l.4: class Empty() +//│ ╙── ^^ +//│ = true +//│ makeEmpty = fun + +:w +new Object +//│ ╔══[WARNING] This 'new' expression partially applies class 'Object' instead of constructing an instance. +//│ ║ l.21: new Object +//│ ║ ^^^^^^^^^^ +//│ ╟── Use `new Object()` to construct an instance, or `=> new Object()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ in ../decls/Prelude.mls +//│ ║ l.7: constructor(value?) +//│ ╙── ^^^^^^ +//│ = fun + +// All-optional lists also accept an empty argument tuple. +:w +class Optional(val value?: Int) +val makeOptional = new Optional +//│ ╔══[WARNING] This 'new' expression partially applies class 'Optional' instead of constructing an instance. +//│ ║ l.35: val makeOptional = new Optional +//│ ║ ^^^^^^^^^^^^ +//│ ╟── Use `new Optional()` to construct an instance, or `=> new Optional()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ l.34: class Optional(val value?: Int) +//│ ╙── ^^^^^^^^^^^^^^^^^ +//│ makeOptional = fun + +// Forward definitions and generic calls must not repeat the source warning. +:w +fun make[A]() = new Forward[A] +class Forward[A]() +[make[Int]()() is Forward, make[Str]()() is Forward] +//│ ╔══[WARNING] This 'new' expression partially applies class 'Forward' instead of constructing an instance. +//│ ║ l.47: fun make[A]() = new Forward[A] +//│ ║ ^^^^^^^^^^^^^^ +//│ ╟── Use `new Forward[A]()` to construct an instance, or `=> new Forward[A]()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ l.48: class Forward[A]() +//│ ╙── ^^ +//│ = [true, true] + +module Classes { class Empty() } +val Alias = Classes.Empty +//│ Alias = fun Empty { class: class Empty } + +:w +new Classes.Empty +//│ ╔══[WARNING] This 'new' expression partially applies class 'Empty' instead of constructing an instance. +//│ ║ l.64: new Classes.Empty +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Use `new Classes.Empty()` to construct an instance, or `=> new Classes.Empty()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ l.59: module Classes { class Empty() } +//│ ╙── ^^ +//│ = fun + +:w +new Alias +//│ ╔══[WARNING] This 'new' expression partially applies class 'Empty' instead of constructing an instance. +//│ ║ l.75: new Alias +//│ ║ ^^^^^^^^^ +//│ ╟── Use `new Alias()` to construct an instance, or `=> new Alias()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ l.59: module Classes { class Empty() } +//│ ╙── ^^ +//│ = fun + +:w +new mut Empty +//│ ╔══[WARNING] This 'new' expression partially applies class 'Empty' instead of constructing an instance. +//│ ║ l.86: new mut Empty +//│ ║ ^^^^^^^^^^^^^ +//│ ╟── Use `new mut Empty()` to construct an instance, or `=> new mut Empty()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ l.4: class Empty() +//│ ╙── ^^ +//│ = fun + +:w +class Box[A](val value?: A) +new Box[Int] +//│ ╔══[WARNING] This 'new' expression partially applies class 'Box' instead of constructing an instance. +//│ ║ l.98: new Box[Int] +//│ ║ ^^^^^^^^^^^^ +//│ ╟── Use `new Box[Int]()` to construct an instance, or `=> new Box[Int]()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ l.97: class Box[A](val value?: A) +//│ ╙── ^^^^^^^^^^^^^^^ +//│ = fun + +// Parameterless classes are constructed; required and multiple lists remain useful partial applications. +class Parameterless +class Required(val value) +class Curried()(val value) + +:expect [true, 1, 2] +val required = new Required +val curried = new Curried +[new Parameterless is Parameterless, required(1).value, curried()(2).value] +//│ = [true, 1, 2] +//│ curried = fun +//│ required = fun + +// The suggested factory suspends construction until each call. +:expect [0, 1, 2] +let count = 0 +class Counted() { set count = count + 1 } +val factory = => new Counted() +let before = count +factory() +let after = count +factory() +[before, after, count] +//│ = [0, 1, 2] +//│ after = 1 +//│ before = 0 +//│ count = 2 +//│ factory = fun + +:expect [true, true] +val makeObject = => new Object() +[new Empty() is Empty, makeObject() is Object] +//│ = [true, true] +//│ makeObject = fun + +// A refinement constructs a subclass, so it is not a partial constructor value. +:expect true +(new Empty() with {}) is Empty +//│ = true + +#lang(strictResolution: false, eagerResolution: false) + +:w +new Empty +//│ ╔══[WARNING] This 'new' expression partially applies class 'Empty' instead of constructing an instance. +//│ ║ l.151: new Empty +//│ ║ ^^^^^^^^^ +//│ ╟── Use `new Empty()` to construct an instance, or `=> new Empty()` to create an explicit zero-argument lambda. +//│ ╟── The constructor accepts an empty argument list. +//│ ║ l.4: class Empty() +//│ ╙── ^^ +//│ = fun diff --git a/hkmc2/shared/src/test/mlscript/slop/RestParameterShapes.mls b/hkmc2/shared/src/test/mlscript/slop/RestParameterShapes.mls new file mode 100644 index 0000000000..10d38e2a91 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/RestParameterShapes.mls @@ -0,0 +1,112 @@ +:. + +// Each definition is compiled before its calls, so argument flow cannot supply +// the rest parameter's initial Array interface. +private fun add(x, ...xs) = xs.reduce(=> _ + _, x) + +:expect [1, 6, "abc"] +[add(1), add(1, 2, 3), add("a", "b", "c")] +//│ = [1, 6, "abc"] + +#lang(strictResolution: true) + +// Private functions do not receive the public-interface exposure pass. +private fun strictAdd(x, ...xs) = xs.reduce(=> _ + _, x) + +:expect [1, 6] +[strictAdd(1), strictAdd(1, 2, 3)] +//│ = [1, 6] + +#lang(strictResolution: false, eagerResolution: true) + +private fun eagerAdd(x, ...xs) = xs.reduce(=> _ + _, x) + +:expect [1, 6] +[eagerAdd(1), eagerAdd(1, 2, 3)] +//│ = [1, 6] + +#lang(strictResolution: true, eagerResolution: true) + +private fun strictEagerAdd(x, ...xs) = xs.reduce(=> _ + _, x) + +:expect [1, 6] +[strictEagerAdd(1), strictEagerAdd(1, 2, 3)] +//│ = [1, 6] + +// Lambdas and a variadic parameter followed by fixed parameters need the same +// initial shape, including when the collected array is empty. +private fun size = (...xs) => xs.length +private fun middle(first, ...xs, last) = xs.length + +:expect [0, 2, 0, 2] +[size(), size(1, 2), middle(1, 2), middle(1, 2, 3, 4)] +//│ = [0, 2, 0, 2] + +// The initial shape must neither hide argument elements nor invent unknown +// elements that strict/eager resolution would reject in these callbacks. +:expect [[1, 2], [3, 4]] +class Item(val value: Int) +private fun values(...xs) = xs.map(x => x.value) +private fun lambdaValues = (...xs) => xs.map(x => x.value) +[values(Item(1), Item(2)), lambdaValues(Item(3), Item(4))] +//│ = [[1, 2], [3, 4]] + +// Returning or forwarding the rest must preserve its element shapes. +:expect [5, 6] +class Entry(val value: Int) +private fun collect(...xs) = xs +private fun forward(...xs) = collect(...xs) +forward(Entry(5), Entry(6)).map(x => x.value) +//│ = [5, 6] + +// A bottom-element seed must not suppress a bad candidate from an actual call. +:ge +class Present(val value: Int) +class Missing +private fun bad(...xs) = xs.map(x => x.value) +bad(Present(1), new Missing) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.66: private fun bad(...xs) = xs.map(x => x.value) +//│ ║ ^^^^^^^ +//│ ╟── Instance of class 'Missing' does not contain member 'value' +//│ ║ l.67: bad(Present(1), new Missing) +//│ ╙── ^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// Written element interfaces continue to hide implementation-only members. +:ge +class Base +class Child(val extra: Int) extends Base +private fun annotated(...xs: Array[Base]) = xs.map(x => x.extra) +annotated(Child(7)) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.80: private fun annotated(...xs: Array[Base]) = xs.map(x => x.extra) +//│ ║ ^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'extra' +//│ ║ l.80: private fun annotated(...xs: Array[Base]) = xs.map(x => x.extra) +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// An explicit tuple annotation retains its distinct positional element types. +:expect [8, 9] +class Left(val left: Int) +class Right(val right: Int) +private fun pair(...xs: [Left, Right]) = [xs.0.left, xs.1.right] +pair(Left(8), Right(9)) +//│ = [8, 9] + +// Separate signatures are annotation boundaries too, including ones that +// intentionally provide no Array members to the implementation. +:ge +private fun opaque: (...Any) -> Int +private fun opaque(...xs) = xs.length +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.102: private fun opaque(...xs) = xs.length +//│ ║ ^^^^^^^^^ +//│ ╟── Cannot resolve member 'length' of a value with unknown shape. +//│ ║ l.101: private fun opaque: (...Any) -> Int +//│ ║ ^^^ +//│ ╟── Type 'Any' is declared without a member interface. +//│ ║ in ../decls/Prelude.mls +//│ ║ l.2: declare type Any +//│ ╙── ^^^ diff --git a/hkmc2/shared/src/test/mlscript/slop/SpreadTupleAnnotations.mls b/hkmc2/shared/src/test/mlscript/slop/SpreadTupleAnnotations.mls new file mode 100644 index 0000000000..dca6a28af3 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/SpreadTupleAnnotations.mls @@ -0,0 +1,84 @@ +:. +#lang(strictResolution: true, eagerResolution: true) + +// A required prefix and a variable tail still expose Array members before calls. +private fun nonEmpty(...xs: [Int, ...Array[Int]]) = xs.length +private fun sum(...xs: [Int, ...Array[Int]]) = xs.reduce(=> _ + _, 0) + +:expect [1, 3, 6] +[nonEmpty(1), nonEmpty(1, 2, 3), sum(1, 2, 3)] +//│ = [1, 3, 6] + +// Spreads retain their annotated element interfaces and exact fixed positions. +:expect [4, 2, true] +type Pair = [Int, Str] +private fun fixed(xs: [...Pair, Bool]) = [xs.0 + 1, xs.1.length, xs.2] +fixed([3, "hi", true]) +//│ = [4, 2, true] + +// Generic tail inference must exclude the fixed prefix and suffix. +:expect [[5, 6], [7]] +class Item(val value: Int) +private fun tail[T](...xs: [Int, ...Array[T]]): Array[T] = xs.slice(1) +private fun middle[T](xs: [Int, ...Array[T], Str]): Array[T] = xs.slice(1, -1) +[tail(0, Item(5), Item(6)).map(x => x.value), + middle([0, Item(7), "end"]).map(x => x.value)] +//│ = [[5, 6], [7]] + +// Adjacent fixed tuple spreads retain separate generic inference positions. +:expect [8, 9] +class Left(val left: Int) +class Right(val right: Int) +private fun pair[A, B](xs: [...[A], ...[B]]): [A, B] = xs +let result = pair([Left(8), Right(9)]) +[result.0.left, result.1.right] +//│ = [8, 9] +//│ result = [Left(8), Right(9)] + +// Several variable spreads retain the fixed fields around and between them. +:expect [1, 5] +private fun many(xs: [Int, ...Array[Int], Int, ...Array[Int], Int]) = [xs.0, xs.length] +many([1, 2, 3, 4, 5]) +//│ = [1, 5] + +// The spread does not erase the required prefix when forwarding arguments. +:ge +private fun zero() = 0 +private fun forward(...xs: [Int, ...Array[Int]]) = zero(...xs) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.47: private fun forward(...xs: [Int, ...Array[Int]]) = zero(...xs) +//│ ║ ^^^^^^^^^^^ +//│ ╟── Function 'zero' expected 0 arguments, but got at least 1 +//│ ║ l.46: private fun zero() = 0 +//│ ╙── ^^^^^^^^^^^^^^^^^^ + +// Annotation boundaries still hide members of supplied implementation types. +:ge +class Base +class Child(val extra: Int) extends Base +private fun annotated(...xs: [Base, ...Array[Base]]) = xs.map(x => x.extra) +annotated(Child(10)) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.59: private fun annotated(...xs: [Base, ...Array[Base]]) = xs.map(x => x.extra) +//│ ║ ^^^^^^^ +//│ ╟── Value of type 'Base' does not contain member 'extra' +//│ ║ l.59: private fun annotated(...xs: [Base, ...Array[Base]]) = xs.map(x => x.extra) +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// Splicing an alias into itself does not guard its recursion. +:ge +type Loop = [Int, ...Loop] +private fun loop(xs: Loop) = xs.length +//│ ╔══[COMPILATION ERROR] Resolution error in tuple literal +//│ ║ l.71: type Loop = [Int, ...Loop] +//│ ║ ^^^^^^^^^^^^^^ +//│ ╟── Unguarded recursive type alias 'Loop'. +//│ ║ l.71: type Loop = [Int, ...Loop] +//│ ║ ^^^^ +//│ ╟── Recursive references must pass through a record, tuple, function, or nominal type. +//│ ║ l.71: type Loop = [Int, ...Loop] +//│ ╙── ^^^^ +//│ ╔══[COMPILATION ERROR] This selection of member 'length' has no resolved target +//│ ║ l.72: private fun loop(xs: Loop) = xs.length +//│ ╙── ^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/slop/TransparentAliasPolarity.mls b/hkmc2/shared/src/test/mlscript/slop/TransparentAliasPolarity.mls new file mode 100644 index 0000000000..e2fbf83f32 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/TransparentAliasPolarity.mls @@ -0,0 +1,68 @@ +:. + +// Alias formals select endpoints relative to their own body. The enclosing +// function domain changes the argument's interpretation, not which endpoint +// the alias's output field selects. +:expect 1 +class Item(val value: Int) +type Source[out T] = {item: T} +private fun read[A](source: Source[A]): A = source.item +read({item: Item(1)}).value +//│ = 1 + +// A contravariant alias's own domain reverses the enclosing domain once more. +:expect 2 +type Sink[in T] = T -> Int +private fun consume[A](f: Sink[A], value: A): Int = f(value) +consume(item => item.value, Item(2)) +//│ = 2 + +// Nested aliases remain transparent under the same enclosing polarity. +:expect 3 +type Indirect[out T] = Source[T] +private fun indirect[A](source: Indirect[A]): A = source.item +indirect({item: Item(3)}).value +//│ = 3 + +// Written bounds still restrict the interface, independently of declaration variance. +:e +fun inputOnly(source: Source[in Item]) = source.item.value +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.29: fun inputOnly(source: Source[in Item]) = source.item.value +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Cannot resolve member 'value' of a value with unknown shape. +//│ ╟── This type argument specifies only an input bound. +//│ ║ l.29: fun inputOnly(source: Source[in Item]) = source.item.value +//│ ║ ^^^^^^^ +//│ ╙── Values read through it have no output bound more specific than 'Any'. + +:expect 4 +private fun outputOnly(source: Source[out Item]) = source.item.value +outputOnly({item: Item(4)}) +//│ = 4 + +// A written projection is equivalent to the covariant argument shorthand. +:expect 5 +private fun projected[A](source: Source[in Nothing out A]): A = source.item +projected({item: Item(5)}).value +//│ = 5 + +// The iterator result keeps the same output through another nominal application. +:expect 6 +type IterSource[out T] = {iterator: () -> Iterator[T]} +private fun iterated[A](source: IterSource[in Nothing out A]): A = source.iterator().next().value +iterated({iterator: () => [Item(6)].iterator()}).value +//│ = 6 + +// For an invariant result constructor, covariance must discard the input flow. +:e +type ArraySource[out T] = {iterator: () -> Array[T]} +private fun connect[A](source: ArraySource[A], value: A) = () +let empty = mut [] +connect({iterator: () => empty}, Item(7)) +empty.0.value +//│ ╔══[COMPILATION ERROR] This selection of member 'value' has no resolved target +//│ ║ l.63: empty.0.value +//│ ╙── ^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. +//│ empty = [] diff --git a/hkmc2/shared/src/test/mlscript/slop/UnsafeUndefined.mls b/hkmc2/shared/src/test/mlscript/slop/UnsafeUndefined.mls new file mode 100644 index 0000000000..e3e1b04257 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/UnsafeUndefined.mls @@ -0,0 +1,245 @@ +:. + + +:w +undefined +//│ ╔══[WARNING] Direct use of 'undefined' is discouraged; use 'unsafe.undefined' for JavaScript interoperation. +//│ ║ l.5: undefined +//│ ╙── ^^^^^^^^^ + + +fun oops = unsafe.undefined + +// * This used to throw, but we now emit `Unit` upon compiling MIR's `return undefined` +:noInline +print(oops) +//│ > () + + +:w +:expect [undefined] +fun bareValue() = [undefined] +bareValue() +//│ ╔══[WARNING] Direct use of 'undefined' is discouraged; use 'unsafe.undefined' for JavaScript interoperation. +//│ ║ l.21: 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 +//│ ╙── ^^^^^^^^^ + +:w +:expect [1, 0] +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 +//│ ╙── ^^^^^^^^^ +//│ = [1, 0] + +// Warning about the spelling must preserve the literal type and its narrowing behavior. +:w +:expect [0, 3] +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 +//│ ╙── ^^^^^^^^^ +//│ = [0, 3] + +// The virtual module emits the host literal without a runtime module lookup. +:sir +:sjs +:expect [undefined, true, false] +[unsafe.undefined, unsafe.undefined === unsafe.undefined, unsafe.undefined === null] +//│ ———————————————| Lowered IR |——————————————————————————————————————————————————————————————————————— +//│ return [undefined, 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)] +//│ = [true, false] + +// Recognition uses the Prelude symbol, not the spelling of the qualifier. +:expect 42 +let unsafe = {id"undefined": 42} in unsafe.undefined +//│ = 42 + + +// These checks inherit strict and eager resolution from slop/.mls. +// Undefined is not a legitimate alternative that can prevent target selection. +:expect [0, 3] +fun nonNullLength(value: Str | null | unsafe.undefined) = + if value is ~null as text then text.length else 0 +[nonNullLength(null), nonNullLength("abc")] +//│ = [0, 3] + +:expect 3 +private fun inferredLength(flag) = + let value = if flag then "abc" else unsafe.undefined + value.length +inferredLength(true) +//│ = 3 + +// Ignore undefined when resolving a call; retain the function's declared result. +:expect 3 +fun callKnown(f: (() -> Str) | unsafe.undefined) = f().length +callKnown(() => "abc") +//│ = 3 + +:expect 3 +private fun inferredCall(flag) = + let f = if flag then () => "abc" else unsafe.undefined + f().length +inferredCall(true) +//│ = 3 + +:expect 3 +fun arrayLength(values: Array[Str] | unsafe.undefined) = values.[0].length +arrayLength(["abc"]) +//│ = 3 + +// Native APIs can return undefined alongside their declared result type. +:expect 3 +fun firstLength(values: Array[Str]) = values.at(0).length +firstLength(["abc"]) +//│ = 3 + +:expect [3] +fun spreadLengths(values: Array[Str] | unsafe.undefined) = [...values].map(text => text.length) +spreadLengths(["abc"]) +//│ = [3] + +// Undefined must not grant dynamic access: a resolved member can use a symbol key. +open annotations { fresh, jsSymbol } +module Keys { + @fresh val read = Symbol("read") +} +class Reader { + @jsSymbol(Keys.read) fun read(): Str = "abc" +} + +:expect 3 +fun readKnown(value: Reader | unsafe.undefined) = value.read().length +readKnown(new Reader) +//│ = 3 + +:expect 3 +private fun construct(flag) = + let cls = if flag then Reader.class else unsafe.undefined + (new cls).read().length +construct(true) +//│ = 3 + +// Projection must retain the real receiver's type arguments. +class Box[A](val item: A) + +:expect 3 +fun projected(value: Box[Str] | unsafe.undefined) = value.Box#item.length +projected(Box("abc")) +//│ = 3 + +: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() +//│ ║ ^^^ +//│ ╟── Function with a declared signature expected 1 argument, but got 0 +//│ ║ l.158: fun wrongArity(f: (Int -> Str) | unsafe.undefined) = f() +//│ ╙── ^^^^^^^^^^ + +// Generic forwarding retains the nullable interface; the unsafe call remains a regression probe. +:re +private fun optionalIdentity(value?) = value +private fun genericIdentity[A](value: A) = optionalIdentity(value) +fun genericLength(value: Str | unsafe.undefined) = + if genericIdentity(value) is ~null as text then text.length else 0 +[genericLength(unsafe.undefined), genericLength("abc")] +//│ ═══[RUNTIME ERROR] TypeError: Cannot read properties of undefined (reading 'length') + +// Aliases and generic arguments retain the legitimate alternatives without +// requiring a value shape for the undefined annotation. +:expect [1, 3] +type Undefined = unsafe.undefined +private fun identity[A](value: A): A = value +fun annotatedLengths(values: Array[Str | Undefined]) = + values.map(value => identity(value).length) +annotatedLengths(["a", "abc"]) +//│ = [1, 3] + +// An ignored value does not erase the surrounding aggregate or its other fields. +:expect [3, 3, [3]] +private fun inferredLengths(flag) = + let values = [if flag then "abc" else unsafe.undefined] + values.map(value => value.length) +let pair = [unsafe.undefined, "abc"] +let record = {absent: unsafe.undefined, text: "abc"} +[pair.1.length, record.text.length, inferredLengths(true)] +//│ = [3, 3, [3]] +//│ pair = [undefined, "abc"] +//│ record = {absent: undefined, text: "abc"} + +// Generic optional forwarding preserves legitimate values and missing defaults. +:expect [3, null] +[genericLength("abc"), optionalIdentity()] +//│ = [3, null] + + +#lang(eagerResolution: true) + +// Optional parameters admit null even when callers only supply other values. +:e +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 +//│ ║ ^^^^^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.207: 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 +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.168: private fun optionalIdentity(value?) = value +//│ ╙── ^^^^^^ + + +#lang(strictResolution: true) + +// Ignoring undefined must not discard either legitimate member target. +class Left(val value: Str) +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 +//│ ║ ^^^^^^^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Right' +//│ ║ l.231: class Right(val value: Str) +//│ ║ ^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Left' +//│ ║ l.230: 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 new file mode 100644 index 0000000000..3a988525f5 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/VariadicParams.mls @@ -0,0 +1,262 @@ +:. +open annotations + + +// *** Variadic parameters followed by trailing parameters *** + +:sjs +fun f(x, ...xs, y) = [x, xs, y] +//│ ————————————| JS (unsanitized) |———————————————————————————————————————————————————————————————————— +//│ let f; +//│ f = function f(x, ...args) { +//│ let y, inlinedVal, tmp, tmp1; +//│ tmp1 = 1 + args.length; +//│ runtime.checkArgs("f", 2, tmp1); +//│ tmp = args.length - 1; +//│ inlinedVal = runtime.safeCall(args.slice(0, tmp)); +//│ y = args.at(-1); +//│ return globalThis.Object.freeze([ x, inlinedVal, y ]) +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + +:expect [[1, [], 2], [1, [2], 3], [1, [2, 3], 4]] +[f(1, 2), f(1, 2, 3), f(1, 2, 3, 4)] +//│ = [[1, [], 2], [1, [2], 3], [1, [2, 3], 4]] + +:e +f(1) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.27: f(1) +//│ ║ ^^^^ +//│ ╟── Function 'f' expected at least 2 arguments, but got 1 +//│ ║ l.8: fun f(x, ...xs, y) = [x, xs, y] +//│ ╙── ^^^^^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] Error: Function 'f' expected at least 2 arguments but got 1 + +fun g(...xs, y, z) = [xs, y, z] + +:expect [[[], 1, 2], [[1, 2], 3, 4]] +[g(1, 2), g(1, 2, 3, 4)] +//│ = [[[], 1, 2], [[1, 2], 3, 4]] + +:expect [1, [2], 3] +((x, ...xs, y) => [x, xs, y])(1, 2, 3) +//│ = [1, [2], 3] + + +// *** Optional parameters before a variadic parameter *** + +// The required parameters take the first and last arguments, the optional ones +// take the remaining ones from left to right, and the variadic one takes the rest. +:sjs +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; +//│ tmp16 = 1 + args7.length; +//│ runtime.checkArgs("h", 2, tmp16); +//│ tmp15 = args7.length - 1; +//│ inlinedVal12 = runtime.safeCall(args7.slice(0, tmp15)); +//│ if (runtime.Tuple.isArrayLike(inlinedVal12) && inlinedVal12.length >= 1) { +//│ y7 = inlinedVal12.at(0); +//│ } else { +//│ y7 = null; +//│ } +//│ if (runtime.Tuple.isArrayLike(inlinedVal12) && inlinedVal12.length >= 2) { +//│ z2 = inlinedVal12.at(1); +//│ } else { +//│ z2 = null; +//│ } +//│ tmp14 = args7.length - 1; +//│ inlinedVal13 = runtime.safeCall(args7.slice(2, tmp14)); +//│ w = args7.at(-1); +//│ return globalThis.Object.freeze([ x, y7, z2, inlinedVal13, w ]) +//│ }; +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + +:expect [[1, null, null, [], 2], [1, 2, null, [], 3]] +[h(1, 2), h(1, 2, 3)] +//│ = [[1, null, null, [], 2], [1, 2, null, [], 3]] + +:expect [[1, 2, 3, [], 4], [1, 2, 3, [4, 5], 6]] +[h(1, 2, 3, 4), h(1, 2, 3, 4, 5, 6)] +//│ = [[1, 2, 3, [], 4], [1, 2, 3, [4, 5], 6]] + + +// *** Explicit `undefined` arguments *** + +// Keep unsafe.undefined calls as compiler regression probes; their observed results are not guaranteed. +f(1, unsafe.undefined, unsafe.undefined) +//│ = [1, [undefined], undefined] + +h(1, unsafe.undefined, 2) +//│ = [1, undefined, null, [], 2] + +h(1, 2, unsafe.undefined, unsafe.undefined, unsafe.undefined) +//│ = [1, 2, undefined, [undefined], undefined] + +:noInline +fun trailingNoInline(x, ...xs, y) = [xs, y] +trailingNoInline(1, unsafe.undefined, unsafe.undefined) +//│ = [[undefined], undefined] + + +// *** Spread arguments *** + +:expect [1, [2, 3], 4] +let arr = [2, 3, 4] +f(1, ...arr) +//│ = [1, [2, 3], 4] +//│ arr = [2, 3, 4] + +:expect [1, [2], 3] +f(...[1, 2], 3) +//│ = [1, [2], 3] + +:expect [[1, 2], 3, 4] +g(...[1, 2, 3, 4]) +//│ = [[1, 2], 3, 4] + + +// *** Resolution of trailing parameters *** + +class A(val a: Int) +class B(val b: Int) + +:expect 2 +private fun lastA(...xs, y) = y.a +lastA(B(0), B(1), A(2)) +//│ = 2 + +:e +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 +//│ ║ ^^^ +//│ ╟── Instance of class 'A' does not contain member 'b' +//│ ║ l.134: lastB(B(0), A(1)) +//│ ╙── ^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// *** Optimizations *** + +:expect 6 +@tailrec fun sum(acc, ...xs, last) = + if xs.length == 0 then acc + last else sum(acc + last, ...xs) +sum(0, 1, 2, 3) +//│ = 6 + +// Omitted optional arguments are `null`, even when their extraction is not inlined. +:noInline +:expect [1, null, [], 2] +fun h(x, y?, ...xs, w) = [x, y, xs, w] +h(1, 2) +//│ = [1, null, [], 2] + +// Stack-safe resumption calls the function again with its saved arguments, which must include +// those of its rest parameter, including the arguments of the trailing parameters lowered into it. +:effectHandlers +:lift +:stackSafe 10 +:expect [[1, 2], 3] +fun deep(n, ...xs, last) = if n == 0 then [xs, last] else deep(n - 1, ...xs, last) +deep(100, 1, 2, 3) +//│ = [[1, 2], 3] + +:effectHandlers +:lift +:stackSafe 10 +:expect [1, 2, 3] +fun deepRest(n, ...xs) = if n == 0 then xs else deepRest(n - 1, ...xs) +deepRest(100, 1, 2, 3) +//│ = [1, 2, 3] + +:effectHandlers +:lift +:stackSafe 10 +:expect [1, [], 2] +fun deepOpt(n, y?, ...xs, last) = if n == 0 then [y, xs, last] else deepOpt(n - 1, y, ...xs, last) +deepOpt(100, 1, 2) +//│ = [1, [], 2] + + +// *** Unknown-length spreads *** + +class Head(val head: Int) +class Middle(val middle: Int) +class Tail(val tail: Int) + +// The first fixed argument always belongs to x, even when the spread is empty. +:expect 1 +private fun last(x, ...middle, y) = y.tail +fun viaUnknown(xs: Array[Tail]) = last(Head(0), ...xs) +viaUnknown([Tail(1)]) +//│ = 1 + +// Unknown lengths remain accepted; runtime checks a count that is too small. +:re +viaUnknown([]) +//│ ═══[RUNTIME ERROR] Error: Function 'last' expected at least 2 arguments but got 1 + +// The middle fixed argument can reach y or the rest, but never the last parameter: +// a successful call still needs an argument after it to fill z. +:expect [2, 3] +private fun lastOfTwo(x, ...middle, y, z) = z.tail +fun viaTwo(xs: Array[Tail]) = lastOfTwo(Head(0), Middle(0), ...xs) +[viaTwo([Tail(2)]), viaTwo([Tail(2), Tail(3)])] +//│ = [2, 3] + +// The same matching rule applies at the other end. +:expect 4 +private fun first(x, ...middle, y) = x.head +fun viaPrefix(xs: Array[Head]) = first(...xs, Tail(0)) +viaPrefix([Head(4)]) +//│ = 4 + +// Several unknown spreads do not make the fixed prefix a possible tail argument. +:expect 6 +fun viaSeveral(xs: Array[Tail], ys: Array[Tail]) = last(Head(0), ...xs, ...ys) +viaSeveral([Tail(5)], [Tail(6)]) +//│ = 6 + +// Optional leading parameters yield to trailing parameters when the count is known. +:expect [[undefined, 7], [8, 9]] +private fun optional(x?, ...middle, y) = [if x is Head as x then x.head else unsafe.undefined, y.tail] +[optional(Tail(7)), optional(Head(8), Tail(9))] +//│ = [[undefined, 7], [8, 9]] + + +// *** 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 +//│ ╙── ^^^^^ + +:e +fun bad(...xs, y?) = 0 +//│ ╔══[COMPILATION ERROR] Optional parameters cannot follow a variadic parameter. +//│ ║ l.241: 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 +//│ ╙── ^^^^^^^^^^^^^ + +: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 +//│ ╙── ^^^^ + +:e +class C(x, ...xs, y) +//│ ╔══[COMPILATION ERROR] Spread parameters are not supported in class parameters. +//│ ║ l.259: class C(x, ...xs, y) +//│ ╙── ^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/slop/codegen/.mls b/hkmc2/shared/src/test/mlscript/slop/codegen/.mls new file mode 100644 index 0000000000..54926c1b7d --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/codegen/.mls @@ -0,0 +1 @@ +#lang(0.3.x) diff --git a/hkmc2/shared/src/test/mlscript/slop/codegen/ComputedSelections.mls b/hkmc2/shared/src/test/mlscript/slop/codegen/ComputedSelections.mls new file mode 100644 index 0000000000..04e7cdf809 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/codegen/ComputedSelections.mls @@ -0,0 +1,20 @@ +:. +:js + +open annotations { fresh, jsSymbol, inline } + +// Inlining must preserve both the local key and the resolved method's receiver. +// Reading the same method through its runtime key remains a dynamic property access. +// Negative array indices count from the end; ordinary property reads do not. +:expect [[41, 41, 41, undefined], [42, 42, 42, undefined]] +@inline +private fun selections(value) = + @fresh val key = Symbol("read") + class Reader(val n) { + @jsSymbol(key) fun read() = n + } + let reader = new Reader(value) + let xs = [0, value] + [reader.read(), reader.(key)(), xs.[-1], xs.(-1)] +[selections(41), selections(42)] +//│ = [[41, 41, 41, undefined], [42, 42, 42, undefined]] diff --git a/hkmc2/shared/src/test/mlscript/slop/codegen/FirstClassVariadicParams.mls b/hkmc2/shared/src/test/mlscript/slop/codegen/FirstClassVariadicParams.mls new file mode 100644 index 0000000000..795c82764c --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/codegen/FirstClassVariadicParams.mls @@ -0,0 +1,56 @@ +:js +:ftc +#lang(0.2.x) + + +// The first-class wrapper forwards to a body that extracts optional and trailing parameters. +:noInline +:expect [[1, null, [], 2], [1, 2, [3, 4], 5]] +fun collect(x, y?, ...xs, last) = [x, y, xs, last] +let f = collect +[f(1, 2), f(1, 2, 3, 4, 5)] +//│ = [[1, null, [], 2], [1, 2, [3, 4], 5]] +//│ f = class anonymous + + +// A foreign declaration has no IR definition from which to obtain lowered parameters. +// Forward the original arguments: the native function interprets its second argument as the radix, +// including when the source signature would assign that argument to the trailing parameter. +declare fun parseInt(s, radix?, ...ignored, last) + +:noInline +:expect [3, 9] +let parse = parseInt +[parse("11", 2), parse("11", 8, 99, 10)] +//│ = [3, 9] +//│ parse = class anonymous + + +// A generated wrapper must leave optional native arguments unnormalized. +declare fun encodeURI(value?) + +:noInline +:expect ["undefined", "undefined", "null", "abc"] +let encode = encodeURI +[encode(), encode(unsafe.undefined), encode(null), encode("abc")] +//│ = ["undefined", "undefined", "null", "abc"] +//│ encode = class anonymous + +// Forwarding a rest parameter must still start after every fixed slot. +declare fun encodeURIComponent(value?, ...ignored) + +:noInline +:expect ["undefined", "undefined", "a%20b"] +let encodeComponent = encodeURIComponent +[encodeComponent(), encodeComponent(unsafe.undefined, 1), encodeComponent("a b", 2, 3)] +//│ = ["undefined", "undefined", "a%20b"] +//│ encodeComponent = class anonymous + + +:noInline +:expect [[null, []], [null, [2]], [1, [2, 3]]] +fun fixedAndRest(x?, ...xs) = [x, xs] +let forwardRest = fixedAndRest +[forwardRest(), forwardRest(unsafe.undefined, 2), forwardRest(1, 2, 3)] +//│ = [[null, []], [null, [2]], [1, [2, 3]]] +//│ forwardRest = class anonymous diff --git a/hkmc2/shared/src/test/mlscript/slop/handlers/VariadicResumption.mls b/hkmc2/shared/src/test/mlscript/slop/handlers/VariadicResumption.mls new file mode 100644 index 0000000000..2d62498eea --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/handlers/VariadicResumption.mls @@ -0,0 +1,37 @@ +:js +:effectHandlers +:lift + + +// * Resuming a function calls it again with its saved arguments, +// * which must include the arguments of its rest parameter. + +abstract class Effect with + fun perform(arg): Any + +:expect [2, 1, [2, 3]] +handle h = Effect with + fun perform(arg)(k) = k(arg + 1) +fun rest(x, ...xs) = + let r = h.perform(x) + [r, x, xs] +rest(1, 2, 3) +//│ = [2, 1, [2, 3]] + +:expect [2, 1, 2, [3], 4] +handle h = Effect with + fun perform(arg)(k) = k(arg + 1) +fun trailing(x, y?, ...xs, z) = + let r = h.perform(x) + [r, x, y, xs, z] +trailing(1, 2, 3, 4) +//│ = [2, 1, 2, [3], 4] + +:expect [2, 1, [2], 3, [4, 5]] +handle h = Effect with + fun perform(arg)(k) = k(arg + 1) +fun curried(x, ...xs)(y, ...ys) = + let r = h.perform(x) + [r, x, xs, y, ys] +curried(1, 2)(3, 4, 5) +//│ = [2, 1, [2], 3, [4, 5]] diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/.mls new file mode 100644 index 0000000000..f246a84b72 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/.mls @@ -0,0 +1,4 @@ +:.. +:js +#lang(strictResolution: true) +#lang(eagerResolution: true) diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/CallbackLayouts.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/CallbackLayouts.mls new file mode 100644 index 0000000000..3198b65ec1 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/CallbackLayouts.mls @@ -0,0 +1,454 @@ +:. + +// *** Optional and droppable callbacks *** + +// A larger body is lowered to a function definition, which checks its arguments at runtime. +:expect [2, 3, 4] +[1, 2, 3].map(x => + let y = x + 1 + let z = y * 2 + let w = z - y + w) +//│ = [2, 3, 4] + +:expect [1, 3, 5] +[1, 2, 3].map((x, i) => x + i) +//│ = [1, 3, 5] + +:expect [2, 3, 4] +fun inc(x) = x + 1 +[1, 2, 3].map(inc) +//│ = [2, 3, 4] + +:expect [[2, 3], 6, true] +let xs = [1, 2, 3] +[xs.filter(x => x > 1), xs.reduce((acc, x) => acc + x, 0), xs.every(x => x > 0)] +//│ = [[2, 3], 6, true] +//│ xs = [1, 2, 3] + +:expect [1, null] +fun app(f: (Int, Int?) -> Any) = f(1) +app((x, y?) => [x, y]) +//│ = [1, null] + +// A callback requiring more arguments than can ever be passed to it is rejected. +:e +[1, 2, 3].map((a, b, c, d) => a) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.36: [1, 2, 3].map((a, b, c, d) => a) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.36: [1, 2, 3].map((a, b, c, d) => a) +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ in ../../decls/Prelude.mls +//│ ║ l.282: fun map[U](f: (T, drop Int, drop Array[T]) -> U): Array[U] +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] Error: Function expected at least 4 arguments but got 3 + +// A callback may not require an argument that callers may omit. +:e +fun app(f: (Int, Int?) -> Any) = f(1) +app((x, y) => [x, y]) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.52: app((x, y) => [x, y]) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.52: app((x, y) => [x, y]) +//│ ║ ^^^^^^^^^^^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.51: fun app(f: (Int, Int?) -> Any) = f(1) +//│ ╙── ^^^^^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] Error: Function expected at least 2 arguments but got 1 + +// A callback may only omit droppable parameters. +:e +fun app(f: (Int, Int?) -> Any) = f(1) +app(x => x) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.67: app(x => x) +//│ ║ ^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.67: app(x => x) +//│ ║ ^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.66: fun app(f: (Int, Int?) -> Any) = f(1) +//│ ╙── ^^^^^^^^^^^^^^^^^^ +//│ = 1 + +:e +fun app(f: (Int, Int) -> Any) = f(1, 2) +app(x => x) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.81: app(x => x) +//│ ║ ^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.81: app(x => x) +//│ ║ ^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.80: fun app(f: (Int, Int) -> Any) = f(1, 2) +//│ ╙── ^^^^^^^^^^^^^^^^^ +//│ = 1 + +// Extra callback parameters must be optional; they are then always `null`. +:expect [1, 2, null] +fun app(f: (Int, Int) -> Any) = f(1, 2) +app((x, y, z?) => [x, y, z]) +//│ = [1, 2, null] + +// Droppable parameters are always passed, so they do not include `null`. +:expect [1, 3, 5] +fun cb(f: (Int, drop Int) -> Int) = f(1, 2) +[cb(x => x), cb((x, i) => x + i), cb((x, i, ...rest) => x + i + rest.length + 2)] +//│ = [1, 3, 5] + +// A callback may use every declared argument, including the array itself. +:expect 3 +[1, 2].reduce((acc, x, i, arr) => acc + arr.length - i, 0) +//│ = 3 + +// Callbacks may omit a droppable rest parameter, but not a non-droppable one. +:expect [1, 3] +fun app(f: (Int, drop ...Array[Int]) -> Int) = f(1, 2, 3) +[app(x => x), app((x, ...xs) => x + xs.length)] +//│ = [1, 3] + +:e +fun app(f: (Int, ...Array[Int]) -> Int) = f(1, 2, 3) +app(x => x) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.118: app(x => x) +//│ ║ ^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.118: app(x => x) +//│ ║ ^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.117: fun app(f: (Int, ...Array[Int]) -> Int) = f(1, 2, 3) +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ = 1 + + +// *** Variadic callback layouts and diagnostic locations *** + +// A fixed interface supplies the last argument to the callback's trailing parameter. +:expect [[2, 2], [2, 2]] +[[1, 2].map((...prefix, arr) => arr.length), [1, 2].map((x, ...middle, arr) => arr.length)] +//│ = [[2, 2], [2, 2]] + +// The same argument mapping applies to a callback with an explicit interface. +:expect [2, 2] +private fun named(...prefix: Array[Any], arr: Array[Int]): Int = arr.length +[1, 2].map(named) +//│ = [2, 2] + +// Different variadic layouts are not inherently incompatible. Keep cheaply known +// positions and the rest's Array interface without enumerating possible lengths. +:expect [3, 1] +fun variadic(f: (Int, ...Array[Int], Str) -> Int) = f(1, 2, "tail") +fun optional(f: (Int?, ...Array[Int], Str) -> Int) = f("tail") +[variadic((...xs) => xs.length), optional((...xs) => xs.length)] +//│ = [3, 1] + +// Diagnose a bad named callback at the call, and show the annotation as context. +:e +private fun needsFour(a, b, c, d) = a +[1].map(needsFour) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.155: [1].map(needsFour) +//│ ║ ^^^^^^^^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.154: private fun needsFour(a, b, c, d) = a +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ in ../../decls/Prelude.mls +//│ ║ l.282: fun map[U](f: (T, drop Int, drop Array[T]) -> U): Array[U] +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] Error: Function 'needsFour' expected at least 4 arguments but got 3 + +// Typed callbacks have the same diagnostic location as inferred callbacks. +:e +val typed: (Int, Int, Array[Int], Int) -> Int = (a, b, c, d) => a +[1].map(typed) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.171: [1].map(typed) +//│ ║ ^^^^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.170: val typed: (Int, Int, Array[Int], Int) -> Int = (a, b, c, d) => a +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ in ../../decls/Prelude.mls +//│ ║ l.282: fun map[U](f: (T, drop Int, drop Array[T]) -> U): Array[U] +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] Error: Function expected at least 4 arguments but got 3 +//│ typed = fun + +// The use site survives nested record constraints. +:e +fun acceptsRecord(r: { callback: (Int?) -> Int }) = 0 +acceptsRecord({ callback: x => x }) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.188: acceptsRecord({ callback: x => x }) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.188: acceptsRecord({ callback: x => x }) +//│ ║ ^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.187: fun acceptsRecord(r: { callback: (Int?) -> Int }) = 0 +//│ ╙── ^^^^^^^^^^^^^ +//│ = 0 + +// Ascriptions identify the supplied implementation, not an invalid function type. +:e +private fun required(x) = x +let bad = required as ((Int?) -> Int) +//│ ╔══[COMPILATION ERROR] Resolution error in member reference +//│ ║ l.203: let bad = required as ((Int?) -> Int) +//│ ║ ^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.202: private fun required(x) = x +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.203: let bad = required as ((Int?) -> Int) +//│ ╙── ^^^^^^^^^^^^^ +//│ bad = fun required + +// Dropping the rest does not permit dropping a non-droppable trailing parameter. +:e +private fun requiredTrailing(f: (drop ...Array[Int], Str) -> Int) = f("tail") +requiredTrailing(() => 0) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.218: requiredTrailing(() => 0) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.218: requiredTrailing(() => 0) +//│ ║ ^^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.217: private fun requiredTrailing(f: (drop ...Array[Int], Str) -> Int) = f("tail") +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ = 0 + +// Callbacks may still collect the trailing input in their rest or bind it at the end. +:expect [1, 4] +private fun collectTrailing(f: (drop ...Array[Int], Str) -> Int) = f("tail") +[collectTrailing((...xs) => xs.length), collectTrailing((...xs, last) => last.length)] +//│ = [1, 4] + +// The same omission is rejected for typed callbacks through a generic alias. +:e +type RequiredTail[A] = (drop ...Array[Int], A) -> Int +private fun genericTrailing[A](tail: A, f: RequiredTail[A]) = f(tail) +val noInputs: () -> Int = () => 0 +genericTrailing("tail", noInputs) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.241: genericTrailing("tail", noInputs) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.240: val noInputs: () -> Int = () => 0 +//│ ║ ^^^^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.238: type RequiredTail[A] = (drop ...Array[Int], A) -> Int +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ = 0 +//│ noInputs = fun + +// An otherwise identical interface may explicitly allow dropping its trailing input. +:expect 0 +type DroppableTail[A] = (drop ...Array[Int], drop A) -> Int +private fun droppableTrailing[A](tail: A, f: DroppableTail[A]) = f(tail) +droppableTrailing("tail", noInputs) +//│ = 0 + +// Leading inputs consume the callback's capacity before any trailing inputs do. +:e +private fun mixedTrailing(f: (Int, drop ...Array[Int], drop Str, Str) -> Int) = f(1, "drop", "keep") +mixedTrailing((first, second) => first) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.264: mixedTrailing((first, second) => first) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.264: mixedTrailing((first, second) => first) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.263: private fun mixedTrailing(f: (Int, drop ...Array[Int], drop Str, Str) -> Int) = f(1, "drop", "keep") +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ = 1 + +// Optional leading inputs can be absent, leaving a trailing input within reach. +// Resolution leaves uncertain layouts to type checking. +:expect 1 +private fun optionalTrailing(f: (Int?, drop ...Array[Int], Str, drop Str) -> Int) = f("keep", "drop") +optionalTrailing(first => 1) +//│ = 1 + + +// *** Matching variadic callbacks *** + +fun variadicTrailing(f: (Int, ...Array[Int], Str) -> Str) = f(1, 2, 3, "a") + +:expect "a" +variadicTrailing((x, ...xs, s) => s) +//│ = "a" + +// A callback cannot discard a non-droppable rest parameter. +:e +variadicTrailing((x, s) => s) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.294: variadicTrailing((x, s) => s) +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.294: variadicTrailing((x, s) => s) +//│ ║ ^^^^^^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.286: fun variadicTrailing(f: (Int, ...Array[Int], Str) -> Str) = f(1, 2, 3, "a") +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ = 2 + + +// *** Extra optional callback inputs *** + +// A callback's extra optional parameter is absent even if a direct call supplied it. +:e +private fun extra(x, s?) = s.length +extra(0, "hi") +fun invoke(f: Int -> Int) = f(0) +invoke(extra) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.311: private fun extra(x, s?) = s.length +//│ ║ ^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.311: private fun extra(x, s?) = s.length +//│ ╙── ^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// Narrowing the absent parameter is valid. +:expect [2, 0] +private fun extraSafe(x, s?) = if s is ~null as s then s.length else 0 +fun invokeSafe(f: Int -> Int) = f(0) +[extraSafe(0, "hi"), invokeSafe(extraSafe)] +//│ = [2, 0] + +// A droppable rest argument can supply an extra optional parameter, but can be empty. +:expect [0, 3] +fun invokeRest(f: (Int, drop ...Array[Str]) -> Int) = [f(0), f(0, "abc")] +invokeRest((x, s?) => if s is ~null as s then s.length else 0) +//│ = [0, 3] + +:e +fun invokeMissingRest(f: (Int, drop ...Array[Str]) -> Int) = f(0) +invokeMissingRest((x, s?) => s.length) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.338: invokeMissingRest((x, s?) => s.length) +//│ ║ ^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.338: invokeMissingRest((x, s?) => s.length) +//│ ╙── ^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// *** Returned callbacks *** + +// A returned callback's arity should be checked once, whether its factory is public or private. +:!js +:e +fun publicFactory(unused): Int -> Int = (x, y) => 0 +//│ ╔══[COMPILATION ERROR] Resolution error in function literal +//│ ║ l.353: fun publicFactory(unused): Int -> Int = (x, y) => 0 +//│ ║ ^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.353: fun publicFactory(unused): Int -> Int = (x, y) => 0 +//│ ║ ^^^^^^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.353: fun publicFactory(unused): Int -> Int = (x, y) => 0 +//│ ╙── ^^^^^^^^^^ + +:!js +:e +private fun privateFactory(unused): Int -> Int = (x, y) => 0 +//│ ╔══[COMPILATION ERROR] Resolution error in function literal +//│ ║ l.366: private fun privateFactory(unused): Int -> Int = (x, y) => 0 +//│ ║ ^^^^^^^^^^^ +//│ ╟── Callback parameter list does not match its declared function type. +//│ ║ l.366: private fun privateFactory(unused): Int -> Int = (x, y) => 0 +//│ ║ ^^^^^^^^^^^ +//│ ╟── The expected function type is declared here. +//│ ║ l.366: private fun privateFactory(unused): Int -> Int = (x, y) => 0 +//│ ╙── ^^^^^^^^^^ + +// Returned callbacks receive their annotated input types even when a factory parameter is unannotated. +:js +:expect 3 +fun typedFactory(unused): Str -> Int = text => text.length +typedFactory(null)("abc") +//│ = 3 + + +// *** Parameter flows across different layouts *** + +// A callback collecting a typed variadic interface must retain its element types. +private fun variadic(f: (...Array[Str]) -> Int) = 0 +variadic((...xs) => xs.map(x => x.length).length) +//│ = 0 + +// A trailing parameter may receive a leading argument, a rest element, or a suffix. +private fun trailing(f: (Str, ...Array[Str]) -> Int) = 0 +trailing((...xs, last) => last.length) +//│ = 0 + +private fun collect(f: (Str, ...Array[Str], Str) -> Int) = 0 +collect((...xs) => xs.map(x => x.length).length) +//│ = 0 + +class Left(val value: Int) +class Right(val value: Int) + +// Uncertain positions must keep both candidates, so strict resolution sees ambiguity. +:e +private fun mixed(f: (Left, ...Array[Right]) -> Int) = 0 +mixed((...xs, last) => last.value) +//│ ╔══[COMPILATION ERROR] This selection of member 'value' is ambiguous, as it has multiple resolved targets +//│ ║ l.407: mixed((...xs, last) => last.value) +//│ ║ ^^^^^^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Right' +//│ ║ l.402: class Right(val value: Int) +//│ ║ ^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Left' +//│ ║ l.401: class Left(val value: Int) +//│ ╙── ^^^^^ +//│ = 0 + +// Consuming an unknown prefix must retain the rest's possible element flows. +private fun dropOne(first, ...rest) = rest +private fun copy(xs: Array[Str]) = dropOne(...xs, "tail").map(x => x.length) + +// The same flow must reach a typed callback's generic element parameter. +:e +#lang(eagerResolution: false) +private fun typed[A](f: (...Array[A]) -> Int, a: A) = + mixed(f) + a.value +//│ ╔══[COMPILATION ERROR] This selection of member 'value' is ambiguous, as it has multiple resolved targets +//│ ║ l.428: a.value +//│ ║ ^^^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Left' +//│ ║ l.401: class Left(val value: Int) +//│ ║ ^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Right' +//│ ║ l.402: class Right(val value: Int) +//│ ╙── ^^^^^ + +// The trailing interface argument must reach the optional callback's second parameter. +#lang(eagerResolution: true) +private fun requiredPair(f: (Str, drop ...Array[Str], drop Str) -> Int) = 0 +requiredPair((first?, second?) => if second is ~null as value then value.length else 0) +//│ = 0 + +// A tuple annotation on the interface's rest must not give a sliced rest its +// original length. Forward the remaining argument without a false arity error. +private fun tupleRest(f: (...[Str, Str]) -> Int) = 0 +private fun length(s: Str) = s.length +tupleRest((first?, ...rest) => length(...rest)) +//│ = 0 + +private fun emptyTupleRest(f: (...[]) -> Int) = 0 +emptyTupleRest((first?, ...rest) => rest.length) +//│ = 0 diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/FunctionSignatures.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/FunctionSignatures.mls new file mode 100644 index 0000000000..0a9dc959ef --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/FunctionSignatures.mls @@ -0,0 +1,257 @@ +:. + +// *** Named function-type parameters *** + +// Names in function types label parameters; their annotations supply the types. +:expect 3 +fun named(f: (s: Str) -> Int) = f("abc") +named(s => s.length) +//│ = 3 + +// Optional named parameters retain both the annotation and the null case. +:expect [0, 3] +fun namedOptional(f: (s?: Str) -> Int) = [f(), f("abc")] +namedOptional((s?) => if s is ~null as s then s.length else 0) +//│ = [0, 3] + +:e +fun missingNamed(f: (s?: Str) -> Int) = f() +missingNamed((s?) => s.length) +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.19: missingNamed((s?) => s.length) +//│ ║ ^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.19: missingNamed((s?) => s.length) +//│ ║ ^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.18: fun missingNamed(f: (s?: Str) -> Int) = f() +//│ ╙── ^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// Normalization also applies through aliases, generics, and droppable parameters. +:expect 3 +type Named[A] = (x: A, drop index: Int) -> Int +fun namedGeneric[A](x: A, f: Named[A]) = f(x, 0) +namedGeneric("abc", x => x.length) +//│ = 3 + +:expect [0, 3] +fun namedSignature: (text?: Str) -> Int +fun namedSignature(text?) = if text is ~null as text then text.length else 0 +[namedSignature(), namedSignature("abc")] +//│ = [0, 3] + + +// *** Separate signatures *** + +fun sig: (Int, Int?) -> Int +fun sig(x, y?) = x + +:e +fun sig2: (Int, Int?) -> Int +fun sig2(x, y) = x +//│ ╔══[COMPILATION ERROR] Implementation parameter list does not match its separate signature. +//│ ║ l.52: fun sig2(x, y) = x +//│ ║ ^^^^^^ +//│ ╟── The signature has different required, optional, variadic, or trailing parameter positions. +//│ ║ l.51: fun sig2: (Int, Int?) -> Int +//│ ╙── ^^^^^^^^^^^^^^^^^^ + +// A separate signature determines how many arguments can be passed. +:e +sig() +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.62: sig() +//│ ║ ^^^^^ +//│ ╟── Function 'sig' expected 1 to 2 arguments, but got 0 +//│ ║ l.48: fun sig(x, y?) = x +//│ ╙── ^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] Error: Function 'sig' expected at least 1 argument but got 0 + +// Aliases must not hide a mismatch between a signature and its implementation. +type OptionalFn = (Int?) -> Int + +:e +private fun aliased: OptionalFn +private fun aliased(x) = x +//│ ╔══[COMPILATION ERROR] Implementation parameter list does not match its separate signature. +//│ ║ l.76: private fun aliased(x) = x +//│ ║ ^^^ +//│ ╟── The signature has different required, optional, variadic, or trailing parameter positions. +//│ ║ l.75: private fun aliased: OptionalFn +//│ ╙── ^^^^^^^^^^ + +:e +private fun tooFew: (Int, Int) -> Int +private fun tooFew(x) = x +//│ ╔══[COMPILATION ERROR] Implementation parameter list does not match its separate signature. +//│ ║ l.86: private fun tooFew(x) = x +//│ ║ ^^^ +//│ ╟── The signature has different required, optional, variadic, or trailing parameter positions. +//│ ║ l.85: private fun tooFew: (Int, Int) -> Int +//│ ╙── ^^^^^^^^^^^^^^^^^ + +:e +private fun tooMany: Int -> Int +private fun tooMany(x, y?) = x +//│ ╔══[COMPILATION ERROR] Implementation parameter list does not match its separate signature. +//│ ║ l.96: private fun tooMany(x, y?) = x +//│ ║ ^^^^^^^ +//│ ╟── The signature has different required, optional, variadic, or trailing parameter positions. +//│ ║ l.95: private fun tooMany: Int -> Int +//│ ╙── ^^^^^^^^^^ + +:e +private fun missingRest: (Int, ...Array[Int], Str) -> Str +private fun missingRest(x, y?) = y +//│ ╔══[COMPILATION ERROR] Implementation parameter list does not match its separate signature. +//│ ║ l.106: private fun missingRest(x, y?) = y +//│ ║ ^^^^^^^ +//│ ╟── The signature has different required, optional, variadic, or trailing parameter positions. +//│ ║ l.105: private fun missingRest: (Int, ...Array[Int], Str) -> Str +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:e +private fun misplacedRest: (Int, ...Array[Int], Str) -> Str +private fun misplacedRest(x, y, ...rest) = "ok" +//│ ╔══[COMPILATION ERROR] Implementation parameter list does not match its separate signature. +//│ ║ l.116: private fun misplacedRest(x, y, ...rest) = "ok" +//│ ║ ^^^^^^^^^^^^^^^ +//│ ╟── The signature has different required, optional, variadic, or trailing parameter positions. +//│ ║ l.115: private fun misplacedRest: (Int, ...Array[Int], Str) -> Str +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:e +private fun extraRest: Int -> Int +private fun extraRest(x, ...rest) = x +//│ ╔══[COMPILATION ERROR] Implementation parameter list does not match its separate signature. +//│ ║ l.126: private fun extraRest(x, ...rest) = x +//│ ║ ^^^^^^^^^^^^ +//│ ╟── The signature has different required, optional, variadic, or trailing parameter positions. +//│ ║ l.125: private fun extraRest: Int -> Int +//│ ╙── ^^^^^^^^^^ + +// A matching generic alias retains its parameter annotations. +:expect 3 +type Unary[A, B] = (value: A) -> B +private fun length: Unary[Str, Int] +private fun length(s) = s.length +length("abc") +//│ = 3 + +// Check each written curried list, including aliases of later arrows. +:e +type Next = (Int?) -> Int +private fun curried: Int -> Next +private fun curried(x)(y) = x +//│ ╔══[COMPILATION ERROR] Implementation parameter list does not match its separate signature. +//│ ║ l.146: private fun curried(x)(y) = x +//│ ║ ^^^ +//│ ╟── The signature has different required, optional, variadic, or trailing parameter positions. +//│ ║ l.145: private fun curried: Int -> Next +//│ ╙── ^^^^^^^^^^^ + + +// *** Variadic and curried signature inputs *** + +// Each part of a separate variadic signature supplies the implementation's interface. +:expect [1, [2, 3], 4] +fun signed: (Str, ...Array[Str], Str) -> [Int, Array[Int], Int] +fun signed(x, ...middle, y) = [x.length, middle.map(s => s.length), y.length] +signed("a", "bb", "ccc", "tail") +//│ = [1, [2, 3], 4] + +// The rest can be empty while the trailing parameter still has its own type. +:expect [1, [], 4] +signed("a", "tail") +//│ = [1, [], 4] + +// Optional leading parameters inherit the union with null. +:expect [[0, [], 4], [2, [3], 4]] +fun optional: (Str?, ...Array[Str], Str) -> Any +fun optional(x?, ...middle, y) = + [if x is ~null as x then x.length else 0, middle.map(s => s.length), y.length] +[optional("tail"), optional("bb", "ccc", "tail")] +//│ = [[0, [], 4], [2, [3], 4]] + +// Generic aliases transport the rest's element annotation as well. +:expect [1, [2], 4] +type Parts[A] = (Str, ...Array[A], Str) -> Any +fun aliased: Parts[Str] +fun aliased(x, ...middle, y) = [x.length, middle.map(s => s.length), y.length] +aliased("a", "bb", "tail") +//│ = [1, [2], 4] + +// Later curried lists use the same mapping. +:expect [1, [2], 4] +fun curried: Int -> (Str, ...Array[Str], Str) -> Any +fun curried(n)(x, ...middle, y) = [n * x.length, middle.map(s => s.length), y.length] +curried(1)("a", "bb", "tail") +//│ = [1, [2], 4] + + +// *** Compound signature inputs *** + +// Every callable component supplies parameter flow, even when its layout differs. +// There are no calls here from which to recover a missing declared input. +type UnionInputs = (Str -> Int) | ((Str, Str) -> Int) +private fun unionInput: UnionInputs +private fun unionInput(...xs) = xs.map(x => x.length).length + +type IntersectionInputs = (Str -> Int) & ((Str, Str) -> Int) +private fun intersectionInput: IntersectionInputs +private fun intersectionInput(...xs) = xs.map(x => x.length).length + +// A disagreement in an earlier list must not erase later curried parameters. +type CurriedInputs = (Int -> (Str -> Int)) | ((Int, Int) -> (Str -> Int)) +private fun curriedInput: CurriedInputs +private fun curriedInput(...xs)(s) = s.length + +// Later lists remain alternatives; inspecting one arrow must not reject its layout. +type CurriedAlternatives = (Int -> (Str -> Int)) | (Int -> ((Str, Str) -> Int)) +private fun curriedAlternatives: CurriedAlternatives +private fun curriedAlternatives(n)(...xs) = xs.map(x => x.length).length + +class Left(val value: Int) +class Right(val value: Int) + +// Both member targets must survive; choosing only the matching arrow hides ambiguity. +:e +private fun unionAmbiguous: (Left -> Int) | ((Right, Int) -> Int) +private fun unionAmbiguous(x) = x.value +//│ ╔══[COMPILATION ERROR] This selection of member 'value' is ambiguous, as it has multiple resolved targets +//│ ║ l.221: private fun unionAmbiguous(x) = x.value +//│ ║ ^^^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Right' +//│ ║ l.216: class Right(val value: Int) +//│ ║ ^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Left' +//│ ║ l.215: class Left(val value: Int) +//│ ╙── ^^^^^ + +:e +private fun intersectionAmbiguous: (Left -> Int) & ((Right, Int) -> Int) +private fun intersectionAmbiguous(x) = x.value +//│ ╔══[COMPILATION ERROR] This selection of member 'value' is ambiguous, as it has multiple resolved targets +//│ ║ l.234: private fun intersectionAmbiguous(x) = x.value +//│ ║ ^^^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Right' +//│ ║ l.216: class Right(val value: Int) +//│ ║ ^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Left' +//│ ║ l.215: class Left(val value: Int) +//│ ╙── ^^^^^ + +// Constraints from the implementation must reach every symbolic domain. +// The callback is never supplied by a call; only the compound signature connects it. +#lang(eagerResolution: false) +private fun domains[A, B](a: A, b: B) = + private fun apply: ((A -> Int) -> Int) & (((B -> Int), Int) -> Int) + private fun apply(f, padding?) = f({first: 1, second: "two"}) + [a.first, b.second] + +// An inline annotation receives constraints without exposing additional members. +private fun explicit[A](a: A) = + private fun read: Str -> Int + private fun read(s: A) = s.length + a.length diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/IterableSpreads.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/IterableSpreads.mls new file mode 100644 index 0000000000..6013c7ad41 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/IterableSpreads.mls @@ -0,0 +1,253 @@ +:. + +// Arrays use their declared iteration method, including through structural Iterable. +:expect [1, 2, 3] +private fun lengths(xs: Iterable[Str]) = [...xs].map(s => s.length) +lengths(["a", "bb", "ccc"]) +//│ = [1, 2, 3] + +// The source alias invokes the real symbol-keyed native method. +:expect "hello" +private fun first(xs: Array[Str]) = xs.iterator().next().value +first(["hello"]) +//│ = "hello" + +// Computed protocol fields and inferred function bodies preserve captured values. +:expect [4, 5] +private fun once(x) = {(Symbol.iterator): () => + let pending = true + {next: () => if pending then + set pending = false + {done: false, value: x} + else {done: true}} +} +class Box(val value: Int) +[...once(Box(4)), ...once(Box(5))].map(x => x.value) +//│ = [4, 5] + +// User methods can implement the same protocol without a result annotation. +:expect ["6"] +class Single(val item: Int) { + @annotations.jsSymbol(Symbol.iterator) fun iterate() = + let pending = true + {next: () => if pending then + set pending = false + {done: false, value: item} + else {done: true}} +} +[...Single(6)].map(n => n.toString()) +//│ = ["6"] + +// Native collections share the same protocol path. +:expect [1, 2, 3] +lengths(new Set(["a", "bb", "ccc"])) +//│ = [1, 2, 3] + +// Every compound interface contributes, even alongside an opaque alternative. +#lang(eagerResolution: false) + +:e +class Left(val value: Int) +class Right(val value: Int) +fun ambiguous(xs: Iterable[Left] | Iterable[Right] | {}) = [...xs].0.value +//│ ╔══[COMPILATION ERROR] This selection of member 'value' is ambiguous, as it has multiple resolved targets +//│ ║ l.52: fun ambiguous(xs: Iterable[Left] | Iterable[Right] | {}) = [...xs].0.value +//│ ║ ^^^^^^^^^^^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Right' +//│ ║ l.51: class Right(val value: Int) +//│ ║ ^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Left' +//│ ║ l.50: class Left(val value: Int) +//│ ╙── ^^^^^ + +#lang(eagerResolution: true) + +// Opaque or unusable protocols are accepted; later type checking can reject them. +fun opaque(xs: {}) = [...xs] +fun opaqueMethod(xs: {(Symbol.iterator): Any}) = [...xs] +type Empty = {} +fun opaqueIterator(xs: {(Symbol.iterator): () -> Empty}) = [...xs] + +// A pending producer must eventually feed the existing spread observation. +:expect [7] +private fun delayed() = [...later()].map(x => x.value) +private fun later() = once(Box(7)) +delayed() +//│ = [7] + +// Recursive forwarding reuses observations and retains the caller's element type. +:expect [8] +private fun forward[A](xs: Iterable[A], n: Int): Array[A] = + if n == 0 then [...xs] else forward([...xs], n - 1) +forward([Box(8)], 3).map(x => x.value) +//│ = [8] + +// Ordinary argument spreads use the same element flow as array construction. +:expect [2, 3] +private fun lengthsOf(...xs) = xs.map(s => s.length) +lengthsOf(...new Set[Str](["aa", "bbb"])) +//│ = [2, 3] + +// Map entries and native string iteration expose their declared element types. +:expect [9] +private fun entries(m: Map[Str, Box]) = [...m].map(pair => pair.1.value) +entries(new Map([["nine", Box(9)]])) +//│ = [9] + +:expect [1, 1] +private fun characters(s: Str) = [...s].map(c => c.length) +characters("ab") +//│ = [1, 1] + +// Inherited source aliases dispatch through the overriding symbol property. +:expect [10, 10, 11] +open class Parent { + @annotations.jsSymbol(Symbol.iterator) open fun iterator() = [Box(0)].iterator() +} +class Child extends Parent { + @annotations.jsSymbol(Symbol.iterator) override fun iterator() = [Box(10)].iterator() +} +class ViaSuper extends Parent { + @annotations.jsSymbol(Symbol.iterator) override fun iterator() = super.iterator() +} +[...(new Child), ...(new Child).iterator(), ...(new ViaSuper)].map(x => if x.value == 0 then 11 else x.value) +//│ = [10, 10, 11] + +// A definite completion result contributes no element, even if it has a value. +:expect [12] +private fun completion() = {(Symbol.iterator): () => ( + {next: () => ({done: true, value: "not an element"})} +)} +[...completion(), Box(12)].map(x => x.value) +//│ = [12] + +// An opaque or incorrectly callable protocol contributes uncertainty, not errors. +fun opaqueNext(xs: {(Symbol.iterator): () -> ({next: Any})}) = [...xs] +fun requiredInputs(xs: {(Symbol.iterator): Int -> ({next: Str -> ({value: Box})})}) = [...xs] + +// Missing done information must not prevent reading a known value. +:expect 13 +private fun partial(xs: {(Symbol.iterator): () -> ({next: () -> ({value: Box})})}) = [...xs].0.value +partial(once(Box(13))) +//│ = 13 + +// Compound results and methods retain every known branch, including with unknowns. +#lang(eagerResolution: false) + +:e +fun intersection(xs: Iterable[Left] & Iterable[Right] & Empty) = [...xs].0.value +//│ ╔══[COMPILATION ERROR] This selection of member 'value' is ambiguous, as it has multiple resolved targets +//│ ║ l.138: fun intersection(xs: Iterable[Left] & Iterable[Right] & Empty) = [...xs].0.value +//│ ║ ^^^^^^^^^^^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Right' +//│ ║ l.51: class Right(val value: Int) +//│ ║ ^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Left' +//│ ║ l.50: class Left(val value: Int) +//│ ╙── ^^^^^ + +:e +fun methodAlternatives(xs: {(Symbol.iterator): (() -> Iterator[Left]) | (() -> Iterator[Right]) | Empty}) = [...xs].0.value +//│ ╔══[COMPILATION ERROR] This selection of member 'value' is ambiguous, as it has multiple resolved targets +//│ ║ l.150: fun methodAlternatives(xs: {(Symbol.iterator): (() -> Iterator[Left]) | (() -> Iterator[Right]) | Empty}) = [...xs].0.value +//│ ║ ^^^^^^^^^^^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Right' +//│ ║ l.51: class Right(val value: Int) +//│ ║ ^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Left' +//│ ║ l.50: class Left(val value: Int) +//│ ╙── ^^^^^ + +:e +fun resultAlternatives(xs: {(Symbol.iterator): () -> ({next: () -> ({value: Left} | {value: Right} | Empty)})}) = [...xs].0.value +//│ ╔══[COMPILATION ERROR] This selection of member 'value' is ambiguous, as it has multiple resolved targets +//│ ║ l.162: fun resultAlternatives(xs: {(Symbol.iterator): () -> ({next: () -> ({value: Left} | {value: Right} | Empty)})}) = [...xs].0.value +//│ ║ ^^^^^^^^^^^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Right' +//│ ║ l.51: class Right(val value: Int) +//│ ║ ^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Left' +//│ ║ l.50: class Left(val value: Int) +//│ ╙── ^^^^^ + +// A known target can still be resolved alongside an opaque protocol alternative. +:expect 14 +private fun partlyKnown(xs: Iterable[Box] | Empty) = [...xs].0.value +partlyKnown([Box(14)]) +//│ = 14 + +#lang(eagerResolution: true) + +// Generic iterable callers keep their own element types through annotations. +:expect [15, 16] +class First(val first: Int) +class Second(val second: Int) +private fun copy[A](xs: Iterable[A]): Array[A] = [...xs] +[copy([First(15)]).0.first, copy(new Set[Second]([Second(16)])).0.second] +//│ = [15, 16] + +// A named iterator method alone does not implement the JavaScript protocol. +:e +fun namedOnly(xs: {iterator: () -> Iterator[Box]}) = [...xs].0.value +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.191: fun namedOnly(xs: {iterator: () -> Iterator[Box]}) = [...xs].0.value +//│ ║ ^^^^^^^^^^^^^^^ +//│ ╟── Cannot resolve member 'value' of a value with unknown shape. +//│ ╟── This spread has no known element shape. +//│ ║ l.191: fun namedOnly(xs: {iterator: () -> Iterator[Box]}) = [...xs].0.value +//│ ╙── ^^ + +// Iterator only promises next; iterable implementations supply their own method. +import "../../../mlscript-compile/Iter.mjs" + +:expect [48, 49] +let count = 48 +let iterator = Iter.IteratorImpl(() => + if count <= 49 then + let value = count + set count = count + 1 + {value: value, done: false} + else {done: true}) +[...iterator] +//│ = [48, 49] +//│ count = 50 +//│ iterator = IteratorImpl(fun) + +:e +private fun requireIterable(iterator: Iterator[Int]) = iterator.iterator() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.217: private fun requireIterable(iterator: Iterator[Int]) = iterator.iterator() +//│ ║ ^^^^^^^^^^^^^^^^^ +//│ ╟── Value of type 'Iterator' does not contain member 'iterator' +//│ ║ l.217: private fun requireIterable(iterator: Iterator[Int]) = iterator.iterator() +//│ ╙── ^^^^^^^^^^^^^ + +// Abstract members retain their source name and runtime key in implementations. +:expect [17] +abstract class AbstractSource { + @annotations.jsSymbol(Symbol.iterator) fun iterator(): Iterator[Int] +} +class ConcreteSource extends AbstractSource { + @annotations.jsSymbol(Symbol.iterator) override fun iterator() = [17].iterator() +} +[...(new ConcreteSource)] +//│ = [17] + +// Ordinary rest parameters receive an empty array in an implicit iterator call. +:expect [1] +private fun restIterator() = {(Symbol.iterator): (...args) => [1].iterator()} +[...restIterator()] +//│ = [1] + +// Resolution leaves protocol compatibility to type checking. An iterator with +// trailing parameters must not trigger the post-lowering rest-parameter assertion. +:!js +private fun trailingIterator() = {(Symbol.iterator): (...args, last) => [1].iterator()} +[...trailingIterator()] + +// The same applies to the implicit call of next, after the iterator is obtained. +:!js +private fun trailingNext() = {(Symbol.iterator): () => ( + {next: (...args, last) => ({done: true})} +)} +[...trailingNext()] diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/LenientCallTargets.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/LenientCallTargets.mls new file mode 100644 index 0000000000..4d305902ed --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/LenientCallTargets.mls @@ -0,0 +1,65 @@ +:. +#lang(eagerResolution: false) + +// Losing a conditional's correlation must not reject a call with a known target. +:expect 1 +class Item(val value: Int) +private fun choose(flag: Bool) = if flag then () => Item(1) else {other: 0} +choose(true)().value +//│ = 1 + +// A non-callable candidate can arrive before a forward callable target. +:expect 2 +private fun forward(flag: Bool) = (if flag then {other: 0} else later)() +private fun later() = Item(2) +forward(false).value +//│ = 2 + +// Callability does not depend on whether a callable publishes a result. +private fun never(flag: Bool) = (if flag then () => throw "stopped" else {other: 0})() + +// With no callable target, lenient resolution still diagnoses the call. +:e +private fun invalid() = {other: 0} +invalid()() +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.24: invalid()() +//│ ║ ^^^^^^^^^^^ +//│ ╟── Record literal cannot be called like a function. +//│ ║ l.23: private fun invalid() = {other: 0} +//│ ╙── ^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +// Known result flows survive uncertainty, so ambiguities still surface. +:e +class Left(val value: Int) +class Right(val value: Int) +private fun alternatives(n: Int) = + if n == 0 then () => Left(1) + else if n == 1 then () => Right(2) + else {other: 0} +alternatives(0)().value +//│ ╔══[COMPILATION ERROR] This selection of member 'value' is ambiguous, as it has multiple resolved targets +//│ ║ l.41: alternatives(0)().value +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Right' +//│ ║ l.36: class Right(val value: Int) +//│ ║ ^^^^^ +//│ ╟── target: value 'value' in overloaded function and class 'Left' +//│ ║ l.35: class Left(val value: Int) +//│ ╙── ^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + +#lang(eagerResolution: true) + +// Eager resolution continues to reject any non-callable alternative. +:e +private fun eager(flag: Bool) = (if flag then () => Item(3) else {other: 0})() +eager(true) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.57: private fun eager(flag: Bool) = (if flag then () => Item(3) else {other: 0})() +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── Record literal cannot be called like a function. +//│ ║ l.57: private fun eager(flag: Bool) = (if flag then () => Item(3) else {other: 0})() +//│ ╙── ^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/NonLocalReturns.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/NonLocalReturns.mls new file mode 100644 index 0000000000..39c925d2c3 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/NonLocalReturns.mls @@ -0,0 +1,115 @@ +:. +:effectHandlers +:lift + +import "../../../mlscript-compile/NonLocalReturnLibrary.mls" + +:expect [42, 5] +[NonLocalReturnLibrary.result[Int](42).0, NonLocalReturnLibrary.result[Str]("hello").0.length] +//│ = [42, 5] + + +// Both normal completion and a non-local return contribute result members. +:expect [42, 0] +fun mkAnswer(value) = {answer: value} +fun answer(early) = + let escape = => return mkAnswer(42) + if early do escape() + mkAnswer(0) +[answer(true).answer, answer(false).answer] +//│ = [42, 0] + +// Returned closures retain the enclosing call's arguments and callable shape. +:expect [42, 5] +fun factory[A](value: A) = + let escape = => return () => value + escape() + throw 0 +[factory[Int](42)(), factory[Str]("hello")().length] +//│ = [42, 5] + +// A local class captures the enclosing function's handler, not a new return scope. +:expect "42" +fun throughClass(value) = + class Worker with + let escape = => return {answer: value} + fun run() = escape() + (new Worker).run() + throw 0 +throughClass(42).answer.toString() +//│ = "42" + +// A nested function's non-local return belongs to that function. +:expect [23, 17] +fun enclosing() = + let escape = => return [inner(), 17] + fun inner() = + let escape = => return 23 + escape() + 99 + escape() + throw 0 +enclosing() +//│ = [23, 17] + +// Getter results use the same flow, even though they have no parameter list. +:expect 42 +val getter = + let escape = => return {answer: 42} + escape() + throw 0 +getter.answer +//│ = 42 +//│ getter = {answer: 42} + +// The shared abortive-handler machinery also transports non-local label payloads. +:expect 42 +val fromLabel = do done: + fun escape() = done.break({answer: 42}) + escape() + throw 0 +fromLabel.answer +//│ = 42 +//│ fromLabel = {answer: 42} + +:expect 3 +fun loop() = + let i = 0 in do done: + fun step() = + if i >= 3 do done.break([i]) + set i += 1 + done.continue + step() +loop().0 +//│ = 3 + +// Resolution must retain both possible targets, rather than dropping either exit. +:e +fun ambiguous(early) = + let escape = => return {answer: 42} + if early do escape() + {answer: 0} +ambiguous(true).answer +//│ ╔══[COMPILATION ERROR] This selection of member 'answer' is ambiguous, as it has multiple resolved targets +//│ ║ l.92: ambiguous(true).answer +//│ ║ ^^^^^^^^^^^^^^^^^^^^^^ +//│ ╟── target: record field 'answer' +//│ ║ l.89: let escape = => return {answer: 42} +//│ ║ ^^^^^^ +//│ ╟── target: record field 'answer' +//│ ║ l.91: {answer: 0} +//│ ╙── ^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// User bindings with runtime names must not capture generated control-flow references. +:expect 3 +fun shadowedLoop(runtime, Runtime, Continue) = + let i = 0 in do done: + fun step() = + if i >= 3 do done.break(i) + set i += 1 + done.continue + step() +shadowedLoop(0, 0, 0) +//│ = 3 diff --git a/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalNull.mls b/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalNull.mls new file mode 100644 index 0000000000..f3785e1b2c --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalNull.mls @@ -0,0 +1,209 @@ +:. +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 new file mode 100644 index 0000000000..33535d286f --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/strictres/OptionalParams.mls @@ -0,0 +1,341 @@ +:. +open annotations + + +// *** Syntax *** + +:pt +fun foo(x, y?) = [x, y] +//│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— +//│ TermDef: +//│ k = Fun +//│ head = App: +//│ lhs = Ident of "foo" +//│ rhs = Tup of Ls of +//│ Ident of "x" +//│ Optional of Ident of "y" +//│ rhs = S of Tup of Ls of +//│ Ident of "x" +//│ Ident of "y" +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + +:pt +fun annotatedFoo(x, y?: Int | Str, z?) = [x, y, z] +//│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— +//│ TermDef: +//│ k = Fun +//│ head = App: +//│ lhs = Ident of "annotatedFoo" +//│ rhs = Tup of Ls of +//│ Ident of "x" +//│ InfixApp: +//│ lhs = Optional of Ident of "y" +//│ kw = Keywrd of keyword ':' +//│ rhs = InfixApp: +//│ lhs = Ident of "Int" +//│ kw = Keywrd of keyword '|' +//│ rhs = Ident of "Str" +//│ Optional of Ident of "z" +//│ rhs = S of Tup of Ls of +//│ Ident of "x" +//│ Ident of "y" +//│ Ident of "z" +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + +:pt +fun app(f: (Int, x: Int, Int?, y?: Int) -> Int) = f(1, 2) +//│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— +//│ TermDef: +//│ k = Fun +//│ head = App: +//│ lhs = Ident of "app" +//│ rhs = Tup of Ls of +//│ InfixApp: +//│ lhs = Ident of "f" +//│ kw = Keywrd of keyword ':' +//│ rhs = InfixApp: +//│ lhs = Tup of Ls of +//│ Ident of "Int" +//│ InfixApp: +//│ lhs = Ident of "x" +//│ kw = Keywrd of keyword ':' +//│ rhs = Ident of "Int" +//│ Optional of Ident of "Int" +//│ InfixApp: +//│ lhs = Optional of Ident of "y" +//│ kw = Keywrd of keyword ':' +//│ rhs = Ident of "Int" +//│ kw = Keywrd of keyword '->' +//│ rhs = Ident of "Int" +//│ rhs = S of App: +//│ lhs = Ident of "f" +//│ rhs = Tup of Ls of +//│ IntLit of 1 +//│ IntLit of 2 +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + + +// *** Calls *** + +:expect [1, null] +foo(1) +//│ = [1, null] + +:expect [1, 2] +foo(1, 2) +//│ = [1, 2] + +:e +foo() +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.89: foo() +//│ ║ ^^^^^ +//│ ╟── Function 'foo' expected 1 to 2 arguments, but got 0 +//│ ║ l.8: fun foo(x, y?) = [x, y] +//│ ╙── ^^^^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] Error: Function 'foo' expected at least 1 argument but got 0 + +:e +foo(1, 2, 3) +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.99: foo(1, 2, 3) +//│ ║ ^^^^^^^^^^^^ +//│ ╟── Function 'foo' expected 1 to 2 arguments, but got 3 +//│ ║ l.8: fun foo(x, y?) = [x, y] +//│ ╙── ^^^^^^^^^^^^^^ +//│ = [1, 2] + +// Extra arguments are ignored at runtime, as in JavaScript. +:expect [1, 2] +fun call(f: Function, ...args) = f(...args) +call(foo, 1, 2, 3) +//│ = [1, 2] + +:re +call(foo) +//│ ═══[RUNTIME ERROR] Error: Function 'foo' expected at least 1 argument but got 0 + +fun bar(x, y?, ...zs) = [x, y, zs] + +:expect [[1, null, []], [1, 2, []], [1, 2, [3, 4]]] +[bar(1), bar(1, 2), bar(1, 2, 3, 4)] +//│ = [[1, null, []], [1, 2, []], [1, 2, [3, 4]]] + +:expect [1, null] +((x, y?) => [x, y])(1) +//│ = [1, null] + + +// *** Body typing *** + +// Without an argument, an optional parameter is `null`. +:e +fun len(s?: Str) = s.length +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.133: fun len(s?: Str) = s.length +//│ ║ ^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.133: fun len(s?: Str) = s.length +//│ ╙── ^^^^^^^ + +:expect [0, 3] +fun len(s?: Str) = if s is ~null as s then s.length else 0 +[len(), len("abc")] +//│ = [0, 3] + +:e +fun len(s?) = s.length +len("abc") +len() +//│ ╔══[COMPILATION ERROR] Resolution error in selection +//│ ║ l.147: fun len(s?) = s.length +//│ ║ ^^^^^^^^ +//│ ╟── Null literal does not contain member 'length' +//│ ║ l.147: fun len(s?) = s.length +//│ ║ ^^ +//│ ╟── Cannot resolve member 'length' of a value with unknown shape. +//│ ╟── Parameter 's' admits values of unknown shape. +//│ ║ l.147: fun len(s?) = s.length +//│ ║ ^^ +//│ ╟── 'len' is exposed by this compilation unit. +//│ ║ l.147: fun len(s?) = s.length +//│ ╙── ^^^^^^^^^^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. + + +// *** Droppable parameters *** + +:pt +fun app(f: (Int, drop Int, drop x: Int) -> Int) = f(1, 2, 3) +//│ ———————————————| Parsed tree |—————————————————————————————————————————————————————————————————————— +//│ TermDef: +//│ k = Fun +//│ head = App: +//│ lhs = Ident of "app" +//│ rhs = Tup of Ls of +//│ InfixApp: +//│ lhs = Ident of "f" +//│ kw = Keywrd of keyword ':' +//│ rhs = InfixApp: +//│ lhs = Tup of Ls of +//│ Ident of "Int" +//│ PrefixApp: +//│ kw = Keywrd of keyword 'drop' +//│ rhs = Ident of "Int" +//│ PrefixApp: +//│ kw = Keywrd of keyword 'drop' +//│ rhs = InfixApp: +//│ lhs = Ident of "x" +//│ kw = Keywrd of keyword ':' +//│ rhs = Ident of "Int" +//│ kw = Keywrd of keyword '->' +//│ rhs = Ident of "Int" +//│ rhs = S of App: +//│ lhs = Ident of "f" +//│ rhs = Tup of Ls of +//│ IntLit of 1 +//│ IntLit of 2 +//│ IntLit of 3 +//│ —————————————————| Output |————————————————————————————————————————————————————————————————————————— + +:expect 1 +fun first(x, drop y) = x +first(1, 2) +//│ = 1 + +// Rest parameters may also be droppable. +:expect 1 +fun first(x, drop ...xs) = x +first(1, 2, 3) +//│ = 1 + +// *** Optimizations *** + +:expect 6 +@tailrec fun sum(n, acc?) = + if n == 0 then acc else sum(n - 1, (if acc is Int then acc else 0) + n) +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 +//│ ╙── ^ + +:e +fun bad(...xs?) = xs +//│ ╔══[COMPILATION ERROR] Variadic parameters cannot be optional. +//│ ║ l.271: 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 +//│ ╙── ^^^^^^^^^^^^^ + +: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?] +//│ ╙── ^^ +//│ ═══[RUNTIME ERROR] This code cannot be run as its compilation yielded an error. +//│ t = undefined + +: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 +//│ ╙── ^^^^ + +: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) +//│ ╙── ^^^ + +// Other operators starting with `?` are unaffected. +:expect 1 +fun (??) orElse(x, y) = if x is unsafe.undefined then y else x +unsafe.undefined ?? 1 +//│ = 1 + +// The optional marker must be attached to its parameter, so `?:` can otherwise be an operator. +:expect 1 +fun (?:) elvis(x, y) = if x is unsafe.undefined then y else x +unsafe.undefined ?: 1 +//│ = 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 +//│ ╙── ^^^^^^^^ + +: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 +//│ ╙── ^ +//│ ╔══[COMPILATION ERROR] Expected a valid parameter, found operator application +//│ ║ l.322: 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) +//│ ╙── ^ +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.332: fun bad(f: (Int, Int ?) -> Int) = f(1) +//│ ║ ^^^^^ +//│ ╟── This type has no resolved target +//│ ║ l.332: fun bad(f: (Int, Int ?) -> Int) = f(1) +//│ ╙── ^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/slop/syntax/.mls b/hkmc2/shared/src/test/mlscript/slop/syntax/.mls new file mode 100644 index 0000000000..54926c1b7d --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/syntax/.mls @@ -0,0 +1 @@ +#lang(0.3.x) diff --git a/hkmc2/shared/src/test/mlscript/slop/syntax/PrefixLambdas.mls b/hkmc2/shared/src/test/mlscript/slop/syntax/PrefixLambdas.mls new file mode 100644 index 0000000000..8633162d82 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/syntax/PrefixLambdas.mls @@ -0,0 +1,88 @@ +:. +:js + + +:expect [42, 3, 7] +val constant = => 42 +val successor = => _ + 1 +val sum = => _ + _ +[constant(), successor(2), sum(3, 4)] +//│ = [42, 3, 7] +//│ constant = fun +//│ successor = fun +//│ sum = fun + +// Each explicit delimiter creates its own function, including empty lists. +:expect [42, 6, 8] +val nested = => (=> 42) +val nestedPlaceholder = => (=> _ + 1) +val mixed = => _ + (=> 3)() +[nested()(), nestedPlaceholder()(5), mixed(5)] +//│ = [42, 6, 8] +//│ mixed = fun +//│ nested = fun +//│ nestedPlaceholder = fun + +// Explicit lambdas capture their environment without evaluating the body early. +:expect [0, 1, 2] +let count = 0 +val next = => { + set count = count + 1 + count +} +let before = count +[before, next(), next()] +//│ = [0, 1, 2] +//│ before = 0 +//│ count = 2 +//│ next = fun + +// Backslash arguments may explicitly suspend a placeholder-free value. +:expect 42 +fun invoke(f) = f() +invoke \ (=> 42) +//│ = 42 + +// A lambda remains a value when passed to a non-backslash call. +:expect 23 +invoke(=> 23) +//│ = 23 + +// Parentheses and blocks preserve the explicit lambda's scope. +:expect 5 +val block = => { + val n = 4 + n + 1 +} +block() +//│ = 5 +//│ block = fun + +// Explicit prefix lambdas have the same non-local return boundary as `() =>`. +:e +fun returning() = => return 17 +//│ ╔══[COMPILATION ERROR] Non-local return statements are only supported with effect handlers enabled. +//│ ║ l.63: fun returning() = => return 17 +//│ ╙── ^^^^^^^^^ + +:effectHandlers +:lift +:expect 17 +fun returning() = + let escape = => return 17 + escape() + 99 +returning() +//│ = 17 + +#lang(useNewResolution: false) + +:effectHandlers +:lift +:expect 17 +fun returning() = + let escape = => return 17 + escape() + 99 +returning() +//│ = 17 diff --git a/hkmc2/shared/src/test/mlscript/slop/ups/regex/.mls b/hkmc2/shared/src/test/mlscript/slop/ups/regex/.mls new file mode 100644 index 0000000000..54926c1b7d --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/ups/regex/.mls @@ -0,0 +1 @@ +#lang(0.3.x) diff --git a/hkmc2/shared/src/test/mlscript/slop/ups/regex/NonStringInputs.mls b/hkmc2/shared/src/test/mlscript/slop/ups/regex/NonStringInputs.mls new file mode 100644 index 0000000000..525ca17d15 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/ups/regex/NonStringInputs.mls @@ -0,0 +1,57 @@ +:. +:js + +pattern Prefix(pattern P) = P ~ "" + +// Prefix matching must reject non-strings before reading their length or +// calling string helpers, including through anonymous pattern arguments. +fun matches(x) = [ + x is "a" ~ "", + x is ("a"..="z") ~ "", + x is Str ~ "", + x is _ ~ "", + x is Prefix("a"..="z") +] + +:expect [true, true, true, true, true] +matches("a") +//│ = [true, true, true, true, true] + +:expect [false, false, false, true, false] +matches("") +//│ = [false, false, false, true, false] + +:expect [false, false, false, false, false] +matches(null) +//│ = [false, false, false, false, false] + +:expect [false, false, false, false, false] +matches(1) +//│ = [false, false, false, false, false] + +:expect [false, false, false, false, false] +matches([]) +//│ = [false, false, false, false, false] + +:expect [false, false, false, false, false] +matches({length: 1}) +//│ = [false, false, false, false, false] + +// Both string-prefix methods and concatenated outputs must survive the +// generated pattern-argument record and MatchSuccess/MatchFailure paths. +fun output(x) = + if x is Prefix(("a"..="z") ~ ("a"..="z")) as result then result else null + +:expect ["ab", null, null] +[output("ab"), output(null), output(1)] +//│ = ["ab", null, null] + +// A rejected string pattern must still try a non-string alternative. +fun fallback(x) = if x is + ("a"..="z") ~ "" then "letter" + Int then "number" + else "other" + +:expect ["letter", "number", "other"] +[fallback("a"), fallback(1), fallback(null)] +//│ = ["letter", "number", "other"] diff --git a/hkmc2/shared/src/test/mlscript/slop/wasm/.mls b/hkmc2/shared/src/test/mlscript/slop/wasm/.mls new file mode 100644 index 0000000000..9e21925992 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/wasm/.mls @@ -0,0 +1,3 @@ +:.. +:!js +:wasm diff --git a/hkmc2/shared/src/test/mlscript/slop/wasm/OptionalParams.mls b/hkmc2/shared/src/test/mlscript/slop/wasm/OptionalParams.mls new file mode 100644 index 0000000000..c1cffa2090 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/wasm/OptionalParams.mls @@ -0,0 +1,235 @@ +:. +:noInline +open annotations +//│ Wasm result: +//│ = {} + +// Direct calls retain supplied arguments and materialize every omitted slot. +fun choose(x: Int, y?: Int) = if y is null then x else y +fun absent(x?) = if x is null then 1 else 0 +fun slots(x?, y?) = absent(x) + absent(y) +//│ Wasm result: +//│ = {} + +:expect 10 +choose(10) +//│ Wasm result: +//│ = 10 + +:expect 20 +choose(10, 20) +//│ Wasm result: +//│ = 20 + +:expect 2 +slots() +//│ Wasm result: +//│ = 2 + +:expect 2 +slots(null) +//│ Wasm result: +//│ = 2 + +:expect 2 +slots(null, null) +//│ Wasm result: +//│ = 2 + +:expect 1 +slots(unsafe.undefined) +//│ Wasm result: +//│ = 1 + +:expect 1 +slots((), null) +//│ Wasm result: +//│ = 1 + +:expect 1 +slots(0) +//│ Wasm result: +//│ = 1 + +:expect 0 +slots(0, false) +//│ Wasm result: +//│ = 0 + +:expect null +fun identity(x?) = x +identity() +//│ Wasm result: +//│ = null + +// Function values need a Wasm callable representation independently of optional arguments. +:fixme +: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] Expected static function reference in Call(...) expression +//│ ║ l.69: indirect(11) +//│ ╙── ^^^^^^^^ + +// Forwarding preserves supplied Unit values and the distinct null value. +:expect 0 +fun forward(x) = absent(x) +forward(unsafe.undefined) +//│ Wasm result: +//│ = 0 + +:expect 1 +forward(null) +//│ Wasm result: +//│ = 1 + +:expect 0 +forward(()) +//│ Wasm result: +//│ = 0 + +// Optional fields are nullable even when their written type is a concrete class. +:expect 12 +class Item(val value: Int) +class Box(val item?: Item) { + fun get() = if item is Item as item then item.value else 0 +} +if Box().get() is 0 then Box(Item(12)).get() else -1 +//│ Wasm result: +//│ = 12 + +// Virtual dispatch and super calls supply omitted arguments independently. +:expect 13 +class Base() { + open fun pick(x?: Int) = if x is null then 13 else x +} +class Child() extends Base() { + override fun pick(x?: Int) = if x is null then super.pick() else x +} +fun dispatch(base: Base) = base.pick() +dispatch(Child()) +//│ Wasm result: +//│ = 13 + +:expect 14 +class Supplied() { + fun pick(x?: Int) = if x is null then 0 else x +} +Supplied().pick(14) +//│ Wasm result: +//│ = 14 + +// Constructor and parent-initializer calls preserve supplied and omitted fields. +:expect 16 +class Fields(val first: Int, val second?: Int) { + fun read() = if second is null then first else second +} +class Inherited() extends Fields(15) +if Inherited().read() is 15 then Fields(1, 16).read() else -1 +//│ Wasm result: +//│ = 16 + +// Required arguments are still checked by resolution. +:e +choose() +//│ ╔══[COMPILATION ERROR] Resolution error in application +//│ ║ l.135: 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 +//│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^ +//│ Wasm result: +//│ = null + +// Retain the unsafe constructor-argument regression; its observed result is not a language guarantee. +:expect 10 +class Normalize(val x?) { + fun read() = absent(x) +} +let count = 0 +fun argument() = + set count = count + 1 + unsafe.undefined +Normalize(argument()).read() + Normalize(argument()).read() * 10 + count * 5 +//│ Wasm result: +//│ = 10 + +// Null and Unit remain distinct; keep the unsafe constructor call as a regression probe too. +:expect 1 +class OptionalField(val x?) { + fun read() = absent(x) +} +let uninitialized = OptionalField(unsafe.undefined).read() +OptionalField(null).read() + OptionalField(()).read() * 10 + uninitialized * 100 +//│ Wasm result: +//│ = 1 + +// TODO: A method call on a class from an earlier Wasm REPL block looks for a struct field. +:fixme +:expect 0 +OptionalField(()).read() +//│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: Missing field `term:OptionalField/read` in struct `member:OptionalField` with type `(type $OptionalField (sub $Object (struct (field $$typeinfo (mut (ref $TypeInfoBase))) (field $x (mut (ref null any))))))` + +// 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 +} +class SuppliedChild() extends SuppliedBase() { + override fun pick(x?) = if x is null then 0 else x +} +fun suppliedDispatch(base: SuppliedBase, x) = base.pick(x) +suppliedDispatch(SuppliedChild(), 17) +//│ Wasm result: +//│ = 17 + +:expect 1112 +class SuppliedBase() { + open fun pick(x?) = if x is null then 1 else x +} +class SuppliedChild() extends SuppliedBase() { + override fun pick(x) = super.pick(x) +} +fun suppliedDispatch(base: SuppliedBase, x) = base.pick(x) + base.pick() +let c = SuppliedChild() +c.pick(10) + suppliedDispatch(c, 100) + suppliedDispatch(SuppliedBase(), 1000) +//│ Wasm result: +//│ = 1112 + +// Optional slots work with inlining and tail-call jumps as well as real calls. +#config(inlining: 100) +//│ Wasm result: +//│ = {} +:expect 1 +@inline fun inlined(x?) = absent(x) +inlined(null) + inlined(()) * 10 + inlined(unsafe.undefined) * 100 +//│ Wasm result: +//│ = 1 + +:expect 1 +inlined() +//│ Wasm result: +//│ = 1 + +:expect 1 +@tailrec fun countdown(n: Int, x?) = + if n is 0 then absent(x) else countdown(n - 1) +countdown(3, 18) +//│ Wasm result: +//│ = 1 + +:expect 0 +@tailrec fun keep(n: Int, x?) = + if n is 0 then absent(x) else keep(n - 1, x) +keep(3, ()) +//│ Wasm result: +//│ = 0 + + +// TODO: improve error +:e +fun test(x?: Int32) = x +//│ ═══[COMPILATION ERROR] Types 'Int32' and 'Unknown' have no common representation +//│ Wasm result: +//│ = {} diff --git a/hkmc2/shared/src/test/mlscript/slop/wasm/ParameterLayouts.mls b/hkmc2/shared/src/test/mlscript/slop/wasm/ParameterLayouts.mls new file mode 100644 index 0000000000..9066d1fcd1 --- /dev/null +++ b/hkmc2/shared/src/test/mlscript/slop/wasm/ParameterLayouts.mls @@ -0,0 +1,39 @@ +// Keep the default resolution mode covered alongside the strict Wasm tests. +#lang(0.3.x) + +:global +:wasm + + +// * Calls pad optional parameters to the fixed Wasm arity. +:expect null +fun opt(x, y?) = y +opt(1) +//│ Wasm result: +//│ = null + +// * Variadic parameters still require packing arguments into an array. +:e +fun rest(x, ...xs) = xs +rest(1, 2) +//│ ╔══[COMPILATION ERROR] Variadic parameters are not supported by the WebAssembly backend. +//│ ║ l.17: fun rest(x, ...xs) = xs +//│ ╙── ^^^^^ + +// * Trailing parameters are lowered into a rest parameter, which is rejected in the same way. +:e +fun trailing(x, ...xs, y) = y +trailing(1, 2, 3) +//│ ╔══[COMPILATION ERROR] Variadic parameters are not supported by the WebAssembly backend. +//│ ║ l.25: fun trailing(x, ...xs, y) = y +//│ ╙── ^^^^^^^^^^^^^ +//│ ╔══[COMPILATION ERROR] More arguments were supplied than the WebAssembly call supports. +//│ ║ l.26: trailing(1, 2, 3) +//│ ╙── ^^^^^^^^^^^^^^^^^ + +// * Constructors bind omitted optional fields to null. +:expect null +class OptionalConstructor(val value?) +(new OptionalConstructor()).value +//│ Wasm result: +//│ = null diff --git a/hkmc2/shared/src/test/mlscript/std/PredefTest.mls b/hkmc2/shared/src/test/mlscript/std/PredefTest.mls index 4f30f622ac..3521cdbf98 100644 --- a/hkmc2/shared/src/test/mlscript/std/PredefTest.mls +++ b/hkmc2/shared/src/test/mlscript/std/PredefTest.mls @@ -194,7 +194,7 @@ foldr(+)() //│ ╔══[COMPILATION ERROR] Resolution error in application //│ ║ l.193: foldr(+)() //│ ║ ^^^^^^^^^^ -//│ ╟── Function 'foldr' expected 1 argument, but got 0 +//│ ╟── Function 'foldr' expected at least 1 argument, but got 0 //│ ║ in ../../mlscript-compile/Predef.mls //│ ║ l.116: fun foldr[A](f: (A, A) -> A)(first: A, ...rest: Array[A]): A = //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ @@ -264,13 +264,13 @@ mkStr of // loose equality is disabled -null == undefined +null == unsafe.undefined //│ = false -undefined == null +unsafe.undefined == null //│ = false -undefined === null +unsafe.undefined === null //│ = false [] == false diff --git a/hkmc2/shared/src/test/mlscript/std/PrettyPrintTest.mls b/hkmc2/shared/src/test/mlscript/std/PrettyPrintTest.mls index 1d9c1103eb..8013bef0dd 100644 --- a/hkmc2/shared/src/test/mlscript/std/PrettyPrintTest.mls +++ b/hkmc2/shared/src/test/mlscript/std/PrettyPrintTest.mls @@ -477,7 +477,7 @@ render of testObject render of testObject, breakLength: null //│ = "{x: {x: {x: {x: {x: 42}}}}, y: \"Hello, world!\"}" -render of testObject, breakLength: undefined +render of testObject, breakLength: unsafe.undefined //│ = "{x: {x: {x: {x: {x: 42}}}}, y: \"Hello, world!\"}" render of testObject, breakLength: () @@ -492,7 +492,7 @@ render of testObject, breakLength: -10 render of testObject, indent: null //│ = "{x: {x: {x: {x: {x: 42}}}}, y: \"Hello, world!\"}" -render of testObject, indent: undefined +render of testObject, indent: unsafe.undefined //│ = "{x: {x: {x: {x: {x: 42}}}}, y: \"Hello, world!\"}" render of testObject, indent: () @@ -510,7 +510,7 @@ render of testObject, padding: true render of testObject, padding: null //│ = "{x: {x: {x: {x: {x: 42}}}}, y: \"Hello, world!\"}" -render of testObject, padding: undefined +render of testObject, padding: unsafe.undefined //│ = "{x: {x: {x: {x: {x: 42}}}}, y: \"Hello, world!\"}" render of testObject, padding: () diff --git a/hkmc2/shared/src/test/mlscript/std/RecordTest.mls b/hkmc2/shared/src/test/mlscript/std/RecordTest.mls index 9b4603bc1f..7a84c0b878 100644 --- a/hkmc2/shared/src/test/mlscript/std/RecordTest.mls +++ b/hkmc2/shared/src/test/mlscript/std/RecordTest.mls @@ -29,7 +29,7 @@ import "../../mlscript-compile/Iter.mls" fun steal(...fields_from: Array) = let from = (fields_from.at(-1) as {from: Object}).from let fields = fields_from.slice(0, -1) - let rcd = new mut Object + let rcd = new mut Object() fields Iter.each of f => set rcd.(f) = from.(f) rcd diff --git a/hkmc2/shared/src/test/mlscript/std/RenderingTest.mls b/hkmc2/shared/src/test/mlscript/std/RenderingTest.mls index e92dc39f2f..bd6d800061 100644 --- a/hkmc2/shared/src/test/mlscript/std/RenderingTest.mls +++ b/hkmc2/shared/src/test/mlscript/std/RenderingTest.mls @@ -8,7 +8,7 @@ render(()) render(null) //│ = "null" -render(undefined) +render(unsafe.undefined) //│ = "undefined" render(123) @@ -371,7 +371,7 @@ renderAsStr of new SelfRef //│ = "SelfRef { a: ref'1, b: 1 } as ref'1" renderAsStr of - new Object with + new Object() with val x = 0 fun y = 0 //│ = "$anon { x: 0 }" @@ -418,7 +418,7 @@ class RangeIterator(val start: Int, val end: Int) extends Iterator with set current += 1 result else - (done: true, value: undefined) + (done: true, value: unsafe.undefined) renderAsStr of RangeIterator(0, 1) //│ = "RangeIterator(0, 1)" diff --git a/hkmc2/shared/src/test/mlscript/syntax/CollectionTransforms.mls b/hkmc2/shared/src/test/mlscript/syntax/CollectionTransforms.mls index 0ae06b4379..4d1b68dd7a 100644 --- a/hkmc2/shared/src/test/mlscript/syntax/CollectionTransforms.mls +++ b/hkmc2/shared/src/test/mlscript/syntax/CollectionTransforms.mls @@ -11,7 +11,7 @@ class Seq[out A] { fun map[B](f: A -> B): Seq[B] = new Seq \ elements.map \ pass1 \ f fun groupMapReduce[B, C](getKey: A -> B, getValue: A -> C, reduce: (C, C) -> C): Map[B, C] = - let groups = new Map[B, C] + let groups = new Map[B, C]() elements.forEach \ pass1 \ e => let key = getKey(e) diff --git a/hkmc2/shared/src/test/mlscript/syntax/annotations/AnnotationPrecedence.mls b/hkmc2/shared/src/test/mlscript/syntax/annotations/AnnotationPrecedence.mls index 43e9e1f3be..2a5c86cca3 100644 --- a/hkmc2/shared/src/test/mlscript/syntax/annotations/AnnotationPrecedence.mls +++ b/hkmc2/shared/src/test/mlscript/syntax/annotations/AnnotationPrecedence.mls @@ -26,25 +26,25 @@ fun foo(x) = if x is @annotations.compile (A(0) as y) then y //│ ╔══[COMPILATION ERROR] Unrecognized pattern (‹erroneous syntax›). //│ ║ l.22: fun foo(x) = if x is @annotations.compile (A(0) as y) then y //│ ╙── ^^^^ +//│ ╔══[COMPILATION ERROR] Name not found: y +//│ ║ l.22: fun foo(x) = if x is @annotations.compile (A(0) as y) then y +//│ ╙── ^ +//│ ╔══[COMPILATION ERROR] Name not found: y +//│ ║ l.22: fun foo(x) = if x is @annotations.compile (A(0) as y) then y +//│ ╙── ^ //│ ╔══[COMPILATION ERROR] Resolution error in application //│ ║ l.22: fun foo(x) = if x is @annotations.compile (A(0) as y) then y //│ ║ ^^^^ //│ ╟── Pattern 'A' cannot be called like a function. //│ ║ l.8: pattern A(pattern B) = B //│ ╙── ^^^^^^^^^^^^^^^^^^^^ -//│ ╔══[COMPILATION ERROR] Name not found: y -//│ ║ l.22: fun foo(x) = if x is @annotations.compile (A(0) as y) then y -//│ ╙── ^ //│ ╔══[COMPILATION ERROR] Resolution error in application //│ ║ l.22: fun foo(x) = if x is @annotations.compile (A(0) as y) then y //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╟── Object 'compile' cannot be called like a function. //│ ║ in ../../decls/Prelude.mls -//│ ║ l.565: object compile +//│ ║ l.597: object compile //│ ╙── ^^^^^^^^^^^^^^ -//│ ╔══[COMPILATION ERROR] Name not found: y -//│ ║ l.22: fun foo(x) = if x is @annotations.compile (A(0) as y) then y -//│ ╙── ^ //│ ╔══[WARNING] This annotation is not supported here. //│ ║ l.22: fun foo(x) = if x is @annotations.compile (A(0) as y) then y //│ ║ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls b/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls index 9f9420adb1..ef811c2b66 100644 --- a/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls +++ b/hkmc2/shared/src/test/mlscript/tailrec/MultiArgLists.mls @@ -321,7 +321,7 @@ fun f(a)(b) = f(a)(b) //│ sym = member:f$tailrec¹ //│ dSym = term:f$tailrec² //│ params = Ls of -//│ ParamList: ‹empty› +//│ LoweredParamList: ‹empty› //│ body = Label: //│ label = label:loopLabel⁰ //│ loop = true @@ -332,12 +332,12 @@ fun f(a)(b) = f(a)(b) //│ sym = member:f⁴ //│ dSym = term:f⁵ //│ params = Ls of -//│ ParamList: +//│ LoweredParamList: //│ params = Ls of //│ Param: //│ sym = a⁰ //│ modulefulness = Modulefulness of N -//│ ParamList: +//│ LoweredParamList: //│ params = Ls of //│ Param: //│ sym = b⁰ diff --git a/hkmc2/shared/src/test/mlscript/ucs/hygiene/HygienicBindings.mls b/hkmc2/shared/src/test/mlscript/ucs/hygiene/HygienicBindings.mls index 34cc193987..d33185c8b6 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/hygiene/HygienicBindings.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/hygiene/HygienicBindings.mls @@ -41,7 +41,7 @@ fun h1(a) = a is None then 0 //│ Normalized: //│ > ‹if|while› -//│ > a is member:Option⁰.Some^term:h1⁰(arg$Some$0$) and +//│ > a is member:Option⁰.Some‹class:Some⁰›^term:h1⁰(arg$Some$0$) and //│ > let x = tmp:arg$Some$0$⁰ //│ > x is member:Left⁰^term:h1⁰(arg$Left$0$) and //│ > let y = tmp:arg$Left$0$⁰ @@ -50,7 +50,7 @@ fun h1(a) = //│ > y is member:Right⁰^term:h1⁰(arg$Right$0$) and //│ > let z = tmp:arg$Right$0$⁰ //│ > else z⁰ -//│ > a is member:Option⁰.None^term:h1⁰ then 0 +//│ > a is member:Option⁰.None‹object:None⁰›^term:h1⁰ then 0 h1(Some(Left(42))) //│ = 42 @@ -73,7 +73,7 @@ fun h2(a) = a is None then 0 //│ Normalized: //│ > ‹if|while› -//│ > a is member:Option⁰.Some^term:h2⁰(arg$Some$0$) and +//│ > a is member:Option⁰.Some‹class:Some⁰›^term:h2⁰(arg$Some$0$) and //│ > let x = tmp:arg$Some$0$¹ //│ > let x' = x⁰ //│ > x' is member:Left⁰^term:h2⁰(arg$Left$0$) and @@ -84,7 +84,7 @@ fun h2(a) = //│ > y' is member:Right⁰^term:h2⁰(arg$Right$0$) and //│ > let z = tmp:arg$Right$0$¹ //│ > else z¹ -//│ > a is member:Option⁰.None^term:h2⁰ then 0 +//│ > a is member:Option⁰.None‹object:None⁰›^term:h2⁰ then 0 // :w fun h3(x, y, f, p) = diff --git a/hkmc2/shared/src/test/mlscript/ucs/normalization/Deduplication.mls b/hkmc2/shared/src/test/mlscript/ucs/normalization/Deduplication.mls index b67bca66db..000fe1839c 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/normalization/Deduplication.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/normalization/Deduplication.mls @@ -501,11 +501,11 @@ fun foo1(x, y, z) = if //│ Normalized: //│ > ‹if|while› //│ > let-split σ = -//│ > z is member:A¹^term:foo1⁰ then member:Predef¹.print^term:foo1⁰(2) -//│ > else member:Predef¹.print^term:foo1⁰(3) +//│ > z is member:A¹^term:foo1⁰ then member:Predef¹.print‹term:Predef⁰/print⁰›^term:foo1⁰(2) +//│ > else member:Predef¹.print‹term:Predef⁰/print⁰›^term:foo1⁰(3) //│ > x is member:A¹^term:foo1⁰ and //│ > let tmp:scrut = member:pred⁰^term:foo1⁰(x¹) -//│ > tmp:scrut is true then member:Predef¹.print^term:foo1⁰(1) +//│ > tmp:scrut is true then member:Predef¹.print‹term:Predef⁰/print⁰›^term:foo1⁰(1) //│ > $σ //│ > $σ @@ -520,15 +520,15 @@ fun foo2(x, y, z) = if //│ > ‹if|while› //│ > let-split σ = //│ > let-split σ = -//│ > z is member:A¹^term:foo2⁰ then member:Predef¹.print^term:foo2⁰(3) -//│ > else member:Predef¹.print^term:foo2⁰(4) +//│ > z is member:A¹^term:foo2⁰ then member:Predef¹.print‹term:Predef⁰/print⁰›^term:foo2⁰(3) +//│ > else member:Predef¹.print‹term:Predef⁰/print⁰›^term:foo2⁰(4) //│ > y is member:A¹^term:foo2⁰ and //│ > let tmp:scrut = member:pred⁰^term:foo2⁰(y²) -//│ > tmp:scrut is true then member:Predef¹.print^term:foo2⁰(2) +//│ > tmp:scrut is true then member:Predef¹.print‹term:Predef⁰/print⁰›^term:foo2⁰(2) //│ > $σ //│ > $σ //│ > x is member:A¹^term:foo2⁰ and //│ > let tmp:scrut = member:pred⁰^term:foo2⁰(x²) -//│ > tmp:scrut is true then member:Predef¹.print^term:foo2⁰(1) +//│ > tmp:scrut is true then member:Predef¹.print‹term:Predef⁰/print⁰›^term:foo2⁰(1) //│ > $σ //│ > $σ diff --git a/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls b/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls index 678223a1be..77de21bb44 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/normalization/ExcessiveDeduplication.mls @@ -54,7 +54,7 @@ if //│ Normalized: //│ > ‹if|while› //│ > let-split σ = y is []=1 and -//│ > let tmp:element0$ = ((tmp:runtime⁰.)Tuple‹module:Tuple⁰›.)get‹term:Tuple⁰/get⁰›(y⁰, 0) +//│ > let tmp:element0$ = tmp:runtime⁰.Tuple‹module:Tuple⁰›.get‹term:Tuple⁰/get⁰›(y⁰, 0) //│ > let b = tmp:element0$⁰ //│ > else //│ > let x⁰ diff --git a/hkmc2/shared/src/test/mlscript/ucs/normalization/OverlapOfPrimitives.mls b/hkmc2/shared/src/test/mlscript/ucs/normalization/OverlapOfPrimitives.mls index a2736e4939..f90c81d4c1 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/normalization/OverlapOfPrimitives.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/normalization/OverlapOfPrimitives.mls @@ -95,7 +95,7 @@ object Foo fun f(arg) = if arg is - undefined then "undefined" + unsafe.undefined then "undefined" null then "null" Object then String(arg) //│ Normalized: @@ -104,7 +104,7 @@ fun f(arg) = if //│ > arg is null then "null" //│ > arg is member:Object⁰^term:f⁰ then member:String⁰^term:f⁰(arg⁰) -f of undefined +f of unsafe.undefined //│ = "undefined" f of null @@ -116,7 +116,7 @@ f of Foo fun f(arg) = if arg is Object then String(arg) - undefined then "undefined" + unsafe.undefined then "undefined" null then "null" //│ Normalized: //│ > ‹if|while› @@ -124,7 +124,7 @@ fun f(arg) = if //│ > arg is undefined then "undefined" //│ > arg is null then "null" -f of undefined +f of unsafe.undefined //│ = "undefined" f of null diff --git a/hkmc2/shared/src/test/mlscript/ucs/normalization/ScrutineeIdentity.mls b/hkmc2/shared/src/test/mlscript/ucs/normalization/ScrutineeIdentity.mls index 148824cf13..22f411afe5 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/normalization/ScrutineeIdentity.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/normalization/ScrutineeIdentity.mls @@ -32,8 +32,8 @@ fun tuple(x) = if //│ Normalized: //│ > ‹if|while› //│ > x is []=2 and -//│ > let tmp:element0$ = ((tmp:runtime⁰.)Tuple‹module:Tuple⁰›.)get‹term:Tuple⁰/get⁰›(x⁰, 0) -//│ > let tmp:element1$ = ((tmp:runtime⁰.)Tuple‹module:Tuple⁰›.)get‹term:Tuple⁰/get⁰›(x⁰, 1) +//│ > let tmp:element0$ = tmp:runtime⁰.Tuple‹module:Tuple⁰›.get‹term:Tuple⁰/get⁰›(x⁰, 0) +//│ > let tmp:element1$ = tmp:runtime⁰.Tuple‹module:Tuple⁰›.get‹term:Tuple⁰/get⁰›(x⁰, 1) //│ > tmp:element0$ is 0 and //│ > let y = tmp:element1$⁰ //│ > tmp:element1$ is 1 then y⁰ diff --git a/hkmc2/shared/src/test/mlscript/ucs/syntax/NestedOpSplits.mls b/hkmc2/shared/src/test/mlscript/ucs/syntax/NestedOpSplits.mls index 99fe0cd4f5..ae687212b6 100644 --- a/hkmc2/shared/src/test/mlscript/ucs/syntax/NestedOpSplits.mls +++ b/hkmc2/shared/src/test/mlscript/ucs/syntax/NestedOpSplits.mls @@ -11,13 +11,13 @@ fun f(x) = //│ Split with nested patterns: //│ > if //│ > let tmp:scrut = 1 -//│ > let tmp:scrut = member:Predef⁰.equals^term:f⁰(x⁰, builtin:+⁰(tmp:scrut⁰, 2)) +//│ > let tmp:scrut = member:Predef⁰.equals‹term:Predef¹/equals⁰›^term:f⁰(x⁰, builtin:+⁰(tmp:scrut⁰, 2)) //│ > tmp:scrut is true then 0 //│ > else 1 //│ Expanded split with flattened patterns: //│ > ‹if|while› //│ > let tmp:scrut = 1 -//│ > let tmp:scrut = member:Predef⁰.equals^term:f⁰(x⁰, builtin:+⁰(tmp:scrut⁰, 2)) +//│ > let tmp:scrut = member:Predef⁰.equals‹term:Predef¹/equals⁰›^term:f⁰(x⁰, builtin:+⁰(tmp:scrut⁰, 2)) //│ > tmp:scrut is true then 0 //│ > else 1 diff --git a/hkmc2/shared/src/test/mlscript/ups/BadPatterns.mls b/hkmc2/shared/src/test/mlscript/ups/BadPatterns.mls index 4dd012109c..cf9e246621 100644 --- a/hkmc2/shared/src/test/mlscript/ups/BadPatterns.mls +++ b/hkmc2/shared/src/test/mlscript/ups/BadPatterns.mls @@ -16,7 +16,7 @@ pattern Foo1[A]: ... :fixme pattern Foo2[pattern A]: ... -//│ ╔══[COMPILATION ERROR] Unsupported type parameter type definition +//│ ╔══[COMPILATION ERROR] Unsupported type parameter shape: type definition //│ ║ l.18: pattern Foo2[pattern A]: ... //│ ╙── ^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Pattern definitions must have a body. diff --git a/hkmc2/shared/src/test/mlscript/ups/Future.mls b/hkmc2/shared/src/test/mlscript/ups/Future.mls index 5eb099d95d..cdec682ae0 100644 --- a/hkmc2/shared/src/test/mlscript/ups/Future.mls +++ b/hkmc2/shared/src/test/mlscript/ups/Future.mls @@ -14,7 +14,7 @@ pattern Rep0[A] = "" | A ~ Rep0[A] // Hypothetical syntax: put pattern parameters in the type parameter list but // prefixed with `pattern`. pattern Rep0[pattern A] = "" | A ~ Rep0[A] -//│ ╔══[COMPILATION ERROR] Unsupported type parameter type definition +//│ ╔══[COMPILATION ERROR] Unsupported type parameter shape: type definition //│ ║ l.16: pattern Rep0[pattern A] = "" | A ~ Rep0[A] //│ ╙── ^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Pattern name not found: A. @@ -61,7 +61,7 @@ pattern GreaterThan(value) = case // Hypothetical syntax (very unlikely to implement): normal view patterns fun view = case n and n > 5 then n - else undefined + else unsafe.undefined fun foo(x) = if x is view as Unit then .... diff --git a/hkmc2/shared/src/test/mlscript/ups/MatchResult.mls b/hkmc2/shared/src/test/mlscript/ups/MatchResult.mls index 2db90be7c6..0f1e0253b7 100644 --- a/hkmc2/shared/src/test/mlscript/ups/MatchResult.mls +++ b/hkmc2/shared/src/test/mlscript/ups/MatchResult.mls @@ -5,10 +5,10 @@ import "../../mlscript-compile/Runtime.mls" open Runtime { MatchSuccess, MatchFailure } -MatchSuccess(0, new Object) +MatchSuccess(0, new Object()) //│ = MatchSuccess(0, {}) -if MatchSuccess(0, new Object) is MatchSuccess(x, _) then x +if MatchSuccess(0, new Object()) is MatchSuccess(x, _) then x //│ = 0 MatchFailure("oops") diff --git a/hkmc2/shared/src/test/mlscript/ups/SimpleTransform.mls b/hkmc2/shared/src/test/mlscript/ups/SimpleTransform.mls index 765cf8bdc5..9c43740e17 100644 --- a/hkmc2/shared/src/test/mlscript/ups/SimpleTransform.mls +++ b/hkmc2/shared/src/test/mlscript/ups/SimpleTransform.mls @@ -153,7 +153,7 @@ pattern EvaluatedTerm = | Infix("/", EvaluatedTerm as left, EvaluatedTerm as right) => left / right | Prefix("-", EvaluatedTerm as operand) => -operand | Prefix("+", EvaluatedTerm as operand) => +operand -//│ Lines of IR: 297 +//│ Lines of IR: 302 // Some helper functions to create `Term`s. fun wrapLiteral(value) = if value is Int then Literal(value) else value diff --git a/hkmc2/shared/src/test/mlscript/ups/fixpoint/SimpleExample.mls b/hkmc2/shared/src/test/mlscript/ups/fixpoint/SimpleExample.mls index cf2f37fcaf..2e75f84150 100644 --- a/hkmc2/shared/src/test/mlscript/ups/fixpoint/SimpleExample.mls +++ b/hkmc2/shared/src/test/mlscript/ups/fixpoint/SimpleExample.mls @@ -259,35 +259,30 @@ pattern Steps = @compile (Step as Steps | _) //│ } //│ method unapplyStringPrefix⁰ = fun unapplyStringPrefix¹(input) { //│ let output, remaining, unapplyResult, output1, unapplyResult1, outputPair, tmp, tmp1, tmp2; -//│ set unapplyResult1 = Step⁰.unapplyStringPrefix﹖(input); -//│ match unapplyResult1 -//│ MatchSuccess⁰ => -//│ set outputPair = unapplyResult1.output⁰; -//│ set output = runtime⁰.Tuple⁰.get⁰(outputPair, 0); -//│ set remaining = runtime⁰.Tuple⁰.get⁰(outputPair, 1); -//│ set unapplyResult = Steps¹.unapply﹖(output); -//│ match unapplyResult +//│ match input +//│ Str⁰ => +//│ set unapplyResult1 = Step⁰.unapplyStringPrefix﹖(input); +//│ match unapplyResult1 //│ MatchSuccess⁰ => -//│ set output1 = unapplyResult.output⁰; -//│ set tmp = [output1, remaining]; -//│ return new runtime⁰.MatchSuccess⁰(tmp, null) -//│ else -//│ match input -//│ Str⁰ => +//│ set outputPair = unapplyResult1.output⁰; +//│ set output = runtime⁰.Tuple⁰.get⁰(outputPair, 0); +//│ set remaining = runtime⁰.Tuple⁰.get⁰(outputPair, 1); +//│ set unapplyResult = Steps¹.unapply﹖(output); +//│ match unapplyResult +//│ MatchSuccess⁰ => +//│ set output1 = unapplyResult.output⁰; +//│ set tmp = [output1, remaining]; +//│ return new runtime⁰.MatchSuccess⁰(tmp, null) +//│ else //│ set tmp1 = [input, ""]; //│ return new runtime⁰.MatchSuccess⁰(tmp1, null) -//│ else -//│ return new runtime⁰.MatchFailure⁰(null) //│ end -//│ end -//│ else -//│ match input -//│ Str⁰ => +//│ else //│ set tmp2 = [input, ""]; //│ return new runtime⁰.MatchSuccess⁰(tmp2, null) -//│ else -//│ return new runtime⁰.MatchFailure⁰(null) //│ end +//│ else +//│ return new runtime⁰.MatchFailure⁰(null) //│ end //│ } //│ }; diff --git a/hkmc2/shared/src/test/mlscript/ups/parametric/EtaConversion.mls b/hkmc2/shared/src/test/mlscript/ups/parametric/EtaConversion.mls index e4f5798707..fe9155f507 100644 --- a/hkmc2/shared/src/test/mlscript/ups/parametric/EtaConversion.mls +++ b/hkmc2/shared/src/test/mlscript/ups/parametric/EtaConversion.mls @@ -13,8 +13,8 @@ pattern Zero = 0 //│ > ‹if|while› //│ > let tmp:scrut = 0 //│ > let tmp:patternArgument0$ = member:Zero⁰ -//│ > let tmp:unapplyResult = (member:Nullable⁰.)unapply(tmp:patternArgument0$⁰, tmp:scrut⁰) -//│ > tmp:unapplyResult is (tmp:runtime⁰.)MatchSuccess‹class:MatchSuccess⁰›(output, bindings) then true +//│ > let tmp:unapplyResult = member:Nullable⁰.unapply(tmp:patternArgument0$⁰, tmp:scrut⁰) +//│ > tmp:unapplyResult is runtime.MatchSuccess(output, bindings) then true //│ > else false //│ = true @@ -24,9 +24,9 @@ import "../../../mlscript-compile/Char.mls" fun foo(x) = x is Nullable(Char.Letter) //│ Normalized: //│ > ‹if|while› -//│ > let tmp:patternArgument0$ = member:Char⁰^term:foo⁰.Letter -//│ > let tmp:unapplyResult = (member:Nullable⁰^term:foo⁰.)unapply(tmp:patternArgument0$¹, x⁰) -//│ > tmp:unapplyResult is (tmp:runtime⁰.)MatchSuccess‹class:MatchSuccess⁰›(output, bindings) then true +//│ > let tmp:patternArgument0$ = member:Char⁰^term:foo⁰.Letter‹pattern:Letter⁰› +//│ > let tmp:unapplyResult = member:Nullable⁰^term:foo⁰.unapply(tmp:patternArgument0$¹, x⁰) +//│ > tmp:unapplyResult is runtime.MatchSuccess(output, bindings) then true //│ > else false foo of null diff --git a/hkmc2/shared/src/test/mlscript/wasm/BuiltinOperators.mls b/hkmc2/shared/src/test/mlscript/wasm/BuiltinOperators.mls index 17c7fa3883..539959c6f6 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/BuiltinOperators.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/BuiltinOperators.mls @@ -63,9 +63,9 @@ wasm.i32.const(1, 2) //│ ║ ^^^^^^^^^^^^^^^^^^^^ //│ ╟── Function 'const' expected 1 argument, but got 2 //│ ║ in ../decls/Prelude.mls -//│ ║ l.449: fun +//│ ║ l.479: fun //│ ║ ^^^ -//│ ║ l.450: const(c: Int): Int32 +//│ ║ l.480: const(c: Int): Int32 //│ ╙── ^^^^^^^^^^^^^^^^^^^^^^^^^^ //│ ╔══[COMPILATION ERROR] Wasm intrinsic 'i32.const' called with incorrect arity (2) //│ ║ l.60: wasm.i32.const(1, 2) diff --git a/hkmc2/shared/src/test/mlscript/wasm/Casts.mls b/hkmc2/shared/src/test/mlscript/wasm/Casts.mls index 0b88fa89ae..adc377e509 100644 --- a/hkmc2/shared/src/test/mlscript/wasm/Casts.mls +++ b/hkmc2/shared/src/test/mlscript/wasm/Casts.mls @@ -181,7 +181,7 @@ fun test: Function = id //│ ╙── ^^ :fixme -fun test: Function = new Object +fun test: Function = new Object() //│ /!!!\ Uncaught error: java.lang.Exception: Internal Error: Missing constructor definition for class member:Object class C diff --git a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala index 0f02d46e4a..3b5ed5af95 100644 --- a/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala +++ b/hkmc2DiffTests/src/test/scala/hkmc2/JSBackendDiffMaker.scala @@ -104,18 +104,6 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: val symbolsToPreserve = definedValues(includeNonTerms = true).iterator.map(_._2).toSet - lazy val blockPrinter = - given ShowCfg = ShowCfg( - showErasedTypes = showIRErasedTypes.isSet, - showExpansionMappings = false, - showFlowSymbols = true, - debug = debug.isSet, - ) - given Elaborator.Ctx = curCtx - Printer() - val print = (p: codegen.Program) => - blockPrinter.worksheet(p)(using irPrintingScp).mkString(output.ColWidth) - Config.extractConfigFromStats(blk).givenIn { val loweringState = summon[Elaborator.State] if file.toString =/= runtimeSourceFile.toString && file.toString =/= preludeFile.toString then @@ -125,6 +113,17 @@ abstract class JSBackendDiffMaker extends MLsDiffMaker: if noCodeGen.isUnset then given Elaborator.Ctx = curCtx given codegen.Erasure = codegen.Erasure(blk) + lazy val blockPrinter = + given ShowCfg = ShowCfg( + showErasedTypes = showIRErasedTypes.isSet, + showExpansionMappings = false, + showFlowSymbols = true, + debug = debug.isSet, + ) + Printer() + val print = (p: codegen.Program) => + blockPrinter.worksheet(p)(using irPrintingScp).mkString(output.ColWidth) + val low = ltl.givenIn: new codegen.Lowering()(using summon[Config], ltl, summon[Raise], loweringState, curCtx, summon[SymbolPrinter]) with codegen.LoweringTraceLog(traceJS.isSet) diff --git a/hkmc2SlopTests/src/test/scala/hkmc2/SlopDiffTestRunner.scala b/hkmc2SlopTests/src/test/scala/hkmc2/SlopDiffTestRunner.scala new file mode 100644 index 0000000000..8504116394 --- /dev/null +++ b/hkmc2SlopTests/src/test/scala/hkmc2/SlopDiffTestRunner.scala @@ -0,0 +1,12 @@ +package hkmc2 + +import org.scalatest.ParallelTestExecution + + +object SlopDiffTestState extends DiffTestRunner.State: + + override def testDir = TestFolders.slopDiffDir(workingDir) + +class SlopDiffTestRunner + extends DiffTestRunnerBase(SlopDiffTestState) + with ParallelTestExecution