diff --git a/docs/syntax/README.md b/docs/syntax/README.md index 999e37169156..24f0404247da 100644 --- a/docs/syntax/README.md +++ b/docs/syntax/README.md @@ -41,8 +41,8 @@ The various components of Enso's syntax are described below: - [**Top-Level Syntax:**](./top-level.md) The syntax at the top-level of an Enso file. - [**Functions:**](./functions.md) The syntax for writing functions in Enso. -- [**Function Arguments:**](./function-arguments.md) The syntax for function - arguments in Enso. +- [**Function Parameters:**](./function-parameters.md) The syntax for function + parameters in Enso. - [**Conversions:**](./conversions.md) The syntax of special _conversion functions_ in Enso. - [**Field Access:**](./projections.md) The syntax for working with fields of diff --git a/docs/syntax/function-arguments.md b/docs/syntax/function-parameters.md similarity index 52% rename from docs/syntax/function-arguments.md rename to docs/syntax/function-parameters.md index 43b0c7b86818..ec78f4c6aa4b 100644 --- a/docs/syntax/function-arguments.md +++ b/docs/syntax/function-parameters.md @@ -1,46 +1,46 @@ --- layout: developer-doc -title: Function Arguments +title: Function Parameters category: syntax tags: [syntax, functions] order: 11 --- -# Function Arguments +# Function Parameters -One of the biggest usability innovations of Enso is the set of argument types -that it supports. The combination of named and defaulted arguments with a +One of the biggest usability innovations of Enso is the set of parameter types +that it supports. The combination of named and defaulted parameters with a curried language creates a tool in which it is very clear to express even complex APIs. -- [Positional Arguments](#positional-arguments) -- [Named Arguments](#named-arguments) -- [Defaulted Arguments](#defaulted-arguments) -- [Optional Arguments](#optional-arguments) -- [Splats Arguments \(Variadics\)](#splats-arguments-variadics) +- [Positional Parameters](#positional-parameters) +- [Named Parameters](#named-parameters) +- [Defaulted Parameters](#defaulted-parameters) +- [Optional Parameters](#optional-parameters) +- [Splats Parameters \(Variadics\)](#splats-parameters-variadics) - [Type Applications](#type-applications) -- [Underscore Arguments](#underscore-arguments) +- [Underscore Parameters](#underscore-parameters) -## Positional Arguments +## Positional Parameters Much like most programming languages, functions in Enso can be called with their -arguments provided positionally. This is the simple case that everybody is +parameters provided positionally. This is the simple case that everybody is familiar with. -## Named Arguments +## Named Parameters -All arguments in Enso are defined with a name. Like all programming languages, -this is necessary for that argument to be used. However, what Enso allows is for -users to then _call_ those arguments by name. +All parameters in Enso are defined with a name. Like all programming languages, +this is necessary for that parameter to be used. However, what Enso allows is +for users to then _call_ those parameters by name. -- An argument is called by name using the syntax `(name = value)` (or one may +- A parameter is called by name using the syntax `(name = value)` (or one may also take advantage of the operator precedence to write `name=value`). - Named arguments are applied in the order they are given. This means that if - you positionally apply to an argument `foo` and then try to later apply to it + you positionally apply to a parameter `foo` and then try to later apply to it by name, this will fail due to currying of functions. - Named arguments _cannot_ be used while using operator syntax. This means that an expression of the form `a + b` cannot apply arguments by name. However, @@ -48,27 +48,29 @@ users to then _call_ those arguments by name. indeed be used (`a.+ (that = b)`). This is a great usability boon as in complex APIs it can often be difficult to -remember the order or arguments. +remember the order or parameters. -## Defaulted Arguments +## Defaulted Parameters Enso also allows users to define their functions with _defaults_ for the -function's arguments. This is very useful for complex APIs as it allows users to -experiment and iterate quickly by only providing the arguments that they want to -customise. +function's parameters. This is very useful for complex APIs as it allows users +to experiment and iterate quickly by only providing the parameters that they +want to customise. -- An argument is defined with a default using the syntax `(name = default_val)`, +- A parameter is defined with a default using the syntax `(name = default_val)`, which, as above, accounts for precedence rules. -- Argument defaults are applied to the function if no argument value is provided - by position or name for that argument. -- Argument defaults are evaluated lazily if the function is lazy in that - argument. +- Parameter defaults are applied to the function if no argument value is + provided by position or name for that parameter. +- Parameter defaults are evaluated lazily if the function is lazy in that + parameter. - We provide a `...` operator which suspends application of the default - arguments for the purposes of currying. + parameters for the purposes of currying. -## Optional Arguments +## Optional Parameters -There are certain cases where the type information for an argument may be able +> [!WARNING] Not implemented. + +There are certain cases where the type information for an parameter may be able to be inferred by the compiler. This is best explained by example. Consider the implementation of a `read` function that reads text and outputs a value of a particular type. @@ -94,7 +96,7 @@ read : Text -> (t=t) -> t read text (this=this) = t.fromText text ``` -This allows users both to provide the argument explicitly or leave it out. In +This allows users both to provide the parameter explicitly or leave it out. In the case where it is not provided, the compiler will attempt to infer it from usage. If this is impossible, an error would be raised. @@ -106,15 +108,17 @@ read : Text -> t? -> t read text this? = t.fromText text ``` -## Splats Arguments (Variadics) +## Splats parameters (Variadics) + +> [!WARNING] Not implemented. Enso provides users with the ability to define variadic functions, or _splats_ functions in our terminology. These are very useful for defining expressive APIs and flexible code. -- These work for both positional and keyword arguments. +- These work for both positional and keyword parameters. - They are defined using the syntax `name...`, where `name` is an arbitrary - argument name. + parameter name. > The actionables for this section are: > @@ -123,25 +127,27 @@ and flexible code. ## Type Applications +> [!WARNING] Not implemented. + There are sometimes cases where the user wants to explicitly refine the type of -an argument at the _call_ site of a function. This can be useful for debugging, -and for writing ad-hoc code. Much like the named-arguments in applications +an parameter at the _call_ site of a function. This can be useful for debugging, +and for writing ad-hoc code. Much like the named-parameters in applications above, Enso also provides a syntax for refining types at the application site. -- To refine an argument type by name at the application site, use the `:=` +- To refine an parameter type by name at the application site, use the `:=` operator (e.g. `arg_name := T`). - This _will_ be type-checked by the compiler, and so `T` must be a valid subtype for the type inferred for (or defined for) the function being called. -## Underscore Arguments +## Underscore parameters -Enso provides the `_` argument as a quick way to create a lambda from a function -call. It obeys the following rules. +Enso provides the `_` parameter as a quick way to create a lambda from a +function call. It obeys the following rules. -- Replacing any function argument with `_` will create a lambda that accepts an - argument and passes it in the place of the underscore. All other function - arguments are applied as normal. +- Replacing any function parameter with `_` will create a lambda that accepts an + parameter and passes it in the place of the underscore. All other function + parameters are applied as normal. - This works both by name and positionally. -- When a function is provided multiple `_` arguments, they are desugared left to - right as the arguments would be applied to the function definition, creating - nested lambdas. +- When a function is provided multiple `_` parameters, they are desugared left + to right as the parameters would be applied to the function definition, + creating nested lambdas. diff --git a/docs/syntax/functions.md b/docs/syntax/functions.md index 7d470c6db345..79c9f41dc7fc 100644 --- a/docs/syntax/functions.md +++ b/docs/syntax/functions.md @@ -422,8 +422,8 @@ works as follows. - Where an argument is not applied to an operator, the missing argument is replaced by an implicit `_`. - The application is then translated based upon the rules for - [underscore arguments](./function-arguments.md#underscore-arguments) described - later. + [underscore parameters](./function-parameters.md#underscore-parameters) + described later. - The whitespace-based precedence rules discussed above also apply to operator sections. diff --git a/docs/types/dynamic-dispatch.md b/docs/types/dynamic-dispatch.md index 1372cfff6e8b..ba59e314e68b 100644 --- a/docs/types/dynamic-dispatch.md +++ b/docs/types/dynamic-dispatch.md @@ -111,6 +111,8 @@ Multiple dispatch is also used on `from` conversions, because in expression > Here, because `Person` conforms to the `HasName` interface, the second `greet` > implementation is chosen because the constraints make it more specific. +TODO: Remove this section? + ## Resolving Clashes on `Any` Special attention must be paid to `Any` and its methods and extension methods. @@ -162,3 +164,213 @@ main = Test.simplify When invoking a method on _module object_ its _module static methods_ take precedence over _instance methods_ defined on `Any`. Thus a module serves primarily as a _container for module (static) methods_. + +## Method Invocation + +**Terminology**: + +- **parameter** is an identifier in a method definition. + - Every parameter has a position and a name. + - Parameters can have default values. +- **argument** is an expression in a method invocation. + - Arguments can be positional or named. + - Named arguments are written as `name=expression`. +- **static method invocation** is a method invocation with `self` named argument + provided. + - Note that `self=expression` does not have to be specified as the first + argument, but is is a good convention to do so. + - Note that with this requirement, we essentially define a special syntax for + _static method invocation_. +- **instance method invocation** is every method invocation that is not + _static_. + - That is, it is a method call without `self` named argument provided. +- **eigen type** of a type `My_Type` is a type of type, usually written as + `My_Type.type`. + - Every type has an eigen type. + - Eigen type of an eigen type is itself. + - There is always exactly one instance of each eigen type, that is, eigen type + is singleton itself. + - The name is motivated by an + [eigen value of a matrix](https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors). +- **singleton type** is a type that has no constructors. + - There can be no atoms of a singleton type. + - The only instance of a _singleton type_ is the type itself. + - `Meta.type_of Singleton_Type` is `Singleton_Type`. + - This is different to `Meta.type_of Normal_Type`, which is + `Normal_Type.type`. +- **associated type** of a module `My_Module` is a type for the module + - It is basically an eigen type for a module. +- **builtin type** is a type annotated with `@Builtin_Type`. + - Builtin type cannot be defined outside standard libraries. + - Builtin types are usually implemented in the engine, and not with pure Enso + code. +- **parent type** of a type `My_Type` is a type that `My_Type` "extends", i.e. + `My_Type` inherits all the methods defined on its parent type. + - Every type has exactly one parent type, except for `Any` type. + - `Any` has no parent type. + - All types have implicit parent type `Any`. + - There are some exceptions for some _builtin types_ + - For example `Float` and `Integer` builtin types have `Number` parent. + - `Number` has `Any` parent. +- **symbol table**. + - Every type has an associated symbol table. + - Symbol table maps symbols to their definitions. + - All definitions are methods. + - Atom constructors are methods. + - Atom fields are methods. More specifically, every atom field has an + associated getter method. + +This section describes the _method invocation_ process, which resolves a +concrete method definition for a concrete call site and evaluates it. For a +method call expression `Receiver.symbol`, this section focuses only on a single +dispatch based on the `Receiver` argument. For multiple dispatch, see the +[Multiple Dispatch](#multiple-dispatch) section. + +This section is further divided into +[Instance method invocation](#instance-method-invocation) and +[Static method invocation](#static-method-invocation). Note the differences +between these types of invocations: + +- Static method invocation has `self` named argument provided. +- Instance method invocation provides `self` argument implicitly. + +### Instance method invocation + +Instance method invocation is any method invocation without `self` named +argument specified (see terminology). Before a method is invoked, it needs to be +_resolved_. Method resolution algorithm for the `Receiver.symbol` expression +first determines the _type_ of the `Receiver`, and then finds the method +definition in its _symbol table_: + +1. **Determine the type of `Receiver`:** + +- 1.1. If `Receiver` is type, the result will be _eigen type_. +- 1.2. If `Receiver` is _singleton type_, the result will be the _type itself_. +- 1.3. If `Receiver` is a value (instance / atom), the result will be the _type + of the value_. +- 1.4. If `Receiver` is a module, the result will be the _associated type_ for + the module. +- 1.5. If `Receiver` is a polyglot object, method resolution and invocation will + be handled according to the [polyglot interoperability](../polyglot/README.md) + rules. + - Polyglot object can be, for example: + - A Java class, imported by `polyglot java import ...` statement. + - Java object instance, created by `Java_Class.new ...` expression. + - Javascript, Python, or any other allowed foreign language object returned + by a foreign method call. + - Refer to [polyglot readme](../polyglot/README.md) for more details. +- 1.6. If there is no `Receiver`, we are just looking for a variable in the + current lexical scope or any parent scopes. See + [Lexical scope lookup](#lexical-scope-lookup). + +2. **Look up symbol in the symbol table of the determined type:** + +- 2.1. Lookup the `symbol` in the Receiver's type and all its parent types. +- 2.2. If it is found, continue to 3. +- 2.3. If it is not found, raise `No_Such_Method` panic and stop. + +3. **Invoke the method with defaulted self argument**: + +- 3.1. `symbol` is a method in Receiver's type (or its parent type) symbol + table. +- 3.2. Such method is treated as if it's first argument is named `self` and has + the preapplied value of `Receiver`. In other words, the method invocation is + equivalent to the `method self=Receiver` expression. + +### Static method invocation + +Static method invocation is any method invocation with `self` named argument +provided (see terminology). Let's consider the following static method +invocation expression: `Receiver.symbol self=receiver`. + +4. **Resolve method on Receiver**: + +- 4.1. Lookup the `symbol` in `Receiver` (not its type!) and all its parent + types. + - Note that if `Receiver` is not a type itself, it has no symbol table, so no + method is found. +- 4.2. If it is found, continue to 5. +- 4.3. If it is not found, raise `No_Such_Method` panic and stop. + +5. **Invoke the method with the provided self argument**: + +- 5.1. Method was found on `Receiver` or its parent type. +- 5.2. Treat the method as if it's first parameter is named `self` and has no + default value. +- 5.3. Bind the `receiver` expression to `self` parameter. + +### Lexical scope lookup + +- If `symbol` is defined in the current lexical scope, select it and stop. +- Iterate parent scopes: from the current lexical scope, up until this module + scope. If `symbol` is defined in the scope, select it and stop. +- Look for the `symbol` in all transitively imported modules (in DFS?). If + `symbol` is defined in any of the modules, select it and stop. +- Raise `Name_Not_Found` panic and stop. + +## Examples + +``` +@Builtin_Type +type Any + to_text self = "???" + +type My_Type + Cons + method self x = x + 1 + +obj = My_Type.Cons +``` + +### Example (a) + +Evaluation of `obj.method 41`: + +- Receiver type is determined as `My_Type` (1.3) +- `method` is looked up in `My_Type` symbol table, and found (2.2) +- `method` is executed as `My_Type.method self=obj x=41` (3.2) +- expression is evaluated to 42. + +### Example (b) + +Evaluation of `My_Type.method obj 41`: + +- Receiver type is determined as `My_Type.type` (1.1) +- There is no `method` in `My_Type.type` symbol table (2.3) +- Raise `No_Such_Method` panic. + +### Example (c) + +Evaluation of `My_Type.method self=obj 41`: + +- Lookup `method` on `My_Type` (4.1). +- `My_Type.method` is found (4.2). +- Execute method as `My_Type.method self=obj x=41` (5.2). +- expression is evaluated to 42. + +### Example (d) + +Evaluation of `Any.to_text obj`: + +- Receiver type is determined as `Any.type` (1.1) +- There is no `Any.type.to_text` method (2.3) +- Raise `No_Such_Method` panic. + +### Example (e) + +Evaluation of `Any.to_text self=obj`: + +- Lookup `to_text` on `Any` (4.1). +- `Any.to_text` is found (4.2). +- Execute method as `Any.to_text self=obj` (5.2). +- expression is evaluated to `"???"`. + +### Example (f) + +Evaluation of `My_Type.to_text`: + +- Receiver type is determined as `My_Type.type` (1.1) +- `to_text` is found in `Any` symbol table (2.2) + - `Any` is parent of `My_Type.type`. +- method is executed as `Any.to_text self=My_Type` (3.2) +- expression is evaluated to `"???"`. diff --git a/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/node/expression/builtin/meta/TypeChainTest.java b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/node/expression/builtin/meta/TypeChainTest.java new file mode 100644 index 000000000000..71b3428af134 --- /dev/null +++ b/engine/runtime-integration-tests/src/test/java/org/enso/interpreter/node/expression/builtin/meta/TypeChainTest.java @@ -0,0 +1,171 @@ +package org.enso.interpreter.node.expression.builtin.meta; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.enso.interpreter.runtime.data.Type; +import org.enso.test.utils.ContextUtils; +import org.graalvm.polyglot.Value; +import org.junit.BeforeClass; +import org.junit.ClassRule; +import org.junit.Ignore; +import org.junit.Test; + +public class TypeChainTest { + @ClassRule public static final ContextUtils ctx = ContextUtils.createDefault(); + private static Value typeOf; + private static Value normalType; + private static Value singletonType; + + @BeforeClass + public static void initTypeOf() { + typeOf = + ctx.evalModule( + """ + import Standard.Base.Meta + + main = Meta.type_of + """); + normalType = + ctx.evalModule( + """ + type Normal_Type + Cons a + + main = Normal_Type + """); + singletonType = + ctx.evalModule( + """ + type Singleton_Type + + main = Singleton_Type + """); + } + + /** {@code allTypes(Text) == [Text, Any]} */ + @Test + public void textChain() { + var type = typeOf.execute("Hello World!"); + var raw = (Type) ctx.unwrapValue(type); + var all = raw.allTypes(ctx.ensoContext()); + + var exp1 = ctx.ensoContext().getBuiltins().text(); + var exp2 = ctx.ensoContext().getBuiltins().any(); + assertArrayEquals("allTypes(Text) == [Text, Any]", new Object[] {exp1, exp2}, all); + } + + /** {@code allTypes(Text.type) == [Text.type, Any]} */ + @Test + public void textTypeChain() { + var textType = typeOf.execute("Ciao"); + var textTypeType = typeOf.execute(textType); + var raw = (Type) ctx.unwrapValue(textTypeType); + var all = raw.allTypes(ctx.ensoContext()); + + var exp1 = ctx.ensoContext().getBuiltins().text().getEigentype(); + var exp2 = ctx.ensoContext().getBuiltins().any(); + assertArrayEquals("allTypes(Text.type) == [Text.type, Any]", new Object[] {exp1, exp2}, all); + } + + /** {@code typeof(Text.type) == Text.type} */ + @Test + public void textEigeintypeChain() { + var textType = typeOf.execute("Ahoj"); + var textTypeType = typeOf.execute(textType); + var loop = typeOf.execute(textTypeType); + assertEquals("Eigentype is the last type - then we loop", textTypeType, loop); + } + + @Test + public void textModuleChain() { + var code = + """ + import Standard.Base.Data.Text + main = Text + """; + var textModule = ctx.evalModule(code); + assertEquals("Standard.Base.Data.Text", textModule.getMetaQualifiedName()); + + var rawType = (Type) ctx.unwrapValue(textModule); + var module = rawType.getDefinitionScope().getModule(); + var associatedType = rawType.getDefinitionScope().getAssociatedType(); + assertEquals("Module's type is its associated type", rawType, associatedType); + assertTrue("Module associated type is eigentype", rawType.isEigenType()); + + var exp1 = module.getScope().getAssociatedType(); + var exp2 = ctx.ensoContext().getBuiltins().any(); + assertArrayEquals( + "Text.type and Any", new Object[] {exp1, exp2}, rawType.allTypes(ctx.ensoContext())); + } + + /** {@code allTypes(Integer) == [Integer, Number, Any]} */ + @Test + public void integerChain() { + var numberType = ctx.ensoContext().getBuiltins().number().getNumber(); + var integerType = ctx.ensoContext().getBuiltins().number().getInteger(); + var anyType = ctx.ensoContext().getBuiltins().any(); + var allTypes = integerType.allTypes(ctx.ensoContext()); + assertArrayEquals( + "allTypes(Integer) == [Integer, Number, Any]", + new Object[] {integerType, numberType, anyType}, + allTypes); + } + + /** {@code allTypes(Any) == [Any]} */ + @Test + public void anyChain() { + var any = ctx.ensoContext().getBuiltins().any(); + var all = any.allTypes(ctx.ensoContext()); + + assertArrayEquals("allTypes(Any) == [Any]", new Object[] {any}, all); + } + + /** {@code allTypes(Any.type) == [Any.type, Any]} */ + @Ignore("Will be addressed in #13939") + @Test + public void anyEigentypeChain() { + var any = ctx.ensoContext().getBuiltins().any(); + var anyType = typeOf.execute(any); + assertEquals("Any.type", anyType.toString()); + var anyTypeType = typeOf.execute(anyType); + assertEquals("Type of Any.type is again Any.type", anyType, anyTypeType); + var raw = (Type) ctx.unwrapValue(anyTypeType); + var all = raw.allTypes(ctx.ensoContext()); + + var anyEigenType = any.getEigentype(); + var anyTypeExpected = ctx.ensoContext().getBuiltins().any(); + assertArrayEquals( + "allTypes(Any.type) == [Any.type, Any]", new Object[] {anyEigenType, anyTypeExpected}, all); + } + + /** {@code allTypes(Normal_Type) == [Normal_Type, Any]} */ + @Test + public void normalTypeChain() { + var raw = (Type) ctx.unwrapValue(normalType); + assertThat("Is not eigen type", raw.isEigenType(), is(false)); + var all = raw.allTypes(ctx.ensoContext()); + + var exp1 = raw; + var exp2 = ctx.ensoContext().getBuiltins().any(); + assertArrayEquals( + "allTypes(Normal_Type) == [Normal_Type, Any]", new Object[] {exp1, exp2}, all); + } + + @Test + public void singletonTypeChain() { + var raw = (Type) ctx.unwrapValue(singletonType); + assertThat("Is eigen type", raw.isEigenType(), is(true)); + var all = raw.allTypes(ctx.ensoContext()); + + var exp1 = raw; + var exp2 = ctx.ensoContext().getBuiltins().any(); + assertArrayEquals( + "allTypes(Singleton_Type.type) == [Singleton_Type.type, Any]", + new Object[] {exp1, exp2}, + all); + } +}