Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions manual/src/features/augmented-assignment.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,22 @@ mixed ++= ["two"];
print(mixed); // [1,"two"]
```

An operand of type `Any` takes a [cast](./casts.md) like anywhere else, as
the operator has to take it:

```ndc,fails
fn opaque(x) -> Any => x;
let values = [1];
values ++= opaque([2]); // error: No function called '++' found that matches the arguments 'List<Int>, Any'
```

```ndc
fn opaque(x) -> Any => x;
let values = [1];
values ++= opaque([2]) as List<Int>;
print(values); // [1,2]
```

## Optimization

You might expect `list ++= [1,2,3]` to desugar to `list = list ++ [1,2,3]`, but that would waste work. Andy C++ handles some augmented assignments directly. In this case, it appends `[1,2,3]` without creating an intermediate list.
Expand Down
16 changes: 16 additions & 0 deletions manual/src/features/casts.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ let ints = values as List<Int>;
print(ints.sum()); // 3
```

A cast is the way a value comes out of an `Any`. Anything goes into an
`Any`, but an `Any` doesn't go where another type is expected, and no
operator or function takes it unless it takes an `Any` (see [Getting a value
out of an `Any`](../reference/types.md#getting-a-value-out-of-an-any)):

```ndc,fails
let value: Any = 5;
value + 1
// error[analysis]: No function called '+' found that matches the arguments 'Any, Int'
```

```ndc
let value: Any = 5;
print((value as Int) + 1); // 6
```

A cast checks the value without converting it. `n as Float` is an error
when `n` is an `Int`; use conversion functions like `float(n)` to change a
value's type. A number literal takes the type the cast says, so `5 as Float`
Expand Down
2 changes: 1 addition & 1 deletion manual/src/reference/memory-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ A closure that captures a variable which holds a reference back to itself create

```ndc
{
let f: Any = ();
let f: Fn(Int) -> Int = fn(x) => 0;
f = fn(x) { if x > 0 { x + f(x - 1) } else { 0 } };
print(f(3)); // 6
}
Expand Down
43 changes: 30 additions & 13 deletions manual/src/reference/types.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,13 +117,13 @@ print(total([1, 2]), total((3, 4)), total(1..4)); // 3 7 6

### Element types are exact

A `List<Int>` is not a `List<Any>`: a `List<Any>` could be given a String, which the list of Ints would then hold. The same goes for maps, tuples, options, the other containers, and function types. To change a container's element type, go through `Any`:
A `List<Int>` is not a `List<Any>`: a `List<Any>` could be given a String, which the list of Ints would then hold. The same goes for maps, tuples, options, the other containers, and function types. To change a container's element type, go through `Any` and a cast:

```ndc,fails
let words = "a b".split(" "); // List<String>
let words = "a b".split(" "); // List<String>
let any: Any = words;
let anything: List<Any> = any; // fine: checked as the program runs
let wrong: List<Any> = words; // error: found List<String> but expected List<Any>
let anything = any as List<Any>; // fine: checked as the program runs
let wrong: List<Any> = words; // error: found List<String> but expected List<Any>
```

### Values of different types
Expand Down Expand Up @@ -169,21 +169,38 @@ used before (or inside) its own declaration, and it goes out of scope together w
block or function that declared it. See [Struct](./types/struct.md) for the scoping
rules.

> **Note:** every value fits `Any`, so an `Any`-annotated binding will accept anything. When a value has no annotation and the analyser can't infer a type, it falls back to `Any`, but values of different types are an error (see [Values of different types](#values-of-different-types)). A parameter of a function declared in the program gets the type of each call's argument instead (see [Generic functions](./types/function.md#generic-functions)). There is also a `Never` type used internally for things like `break` that don't produce a value — you'll rarely need to write it by hand.
> **Note:** every value fits `Any`, so an `Any`-annotated binding will accept anything. When a value has no annotation and the analyser can't infer a type, it falls back to `Any`, which then takes a cast to be used as anything else (see below), and values of different types are an error (see [Values of different types](#values-of-different-types)). A parameter of a function declared in the program gets the type of each call's argument instead (see [Generic functions](./types/function.md#generic-functions)). There is also a `Never` type used internally for things like `break` that don't produce a value — you'll rarely need to write it by hand.

### Using an `Any` value where a type is expected
### Getting a value out of an `Any`

You can use an `Any` value where a specific type is expected. The program checks the value when it gets there:
Anything goes into an `Any`, but getting a value back out takes a [cast](../features/casts.md): `value as Int` checks as the program runs that the value is an Int, and `value as? Int` gives an `Option<Int>`. An `Any` doesn't go where another type is expected without one:

```ndc,fails
let a: Any = 5;
let n: Int = a; // fine: a holds an Int
print(n + 1); // 6
let n: Int = a; // error: mismatched types: found Any but expected Int
```

```ndc
let a: Any = 5;
let n: Int = a as Int; // checked: a holds an Int
print(n + 1); // 6
print(a as? String); // None
```

The same goes for assigning it, passing it to a function, returning it from one, iterating over it, calling it, and using an operator on it: each needs something that takes an `Any`, or a cast.

let b: Any = "five";
let m: Int = b; // error: expected Int but found String
```ndc,fails
let a: Any = 5;
a + 1 // error: No function called '+' found that matches the arguments 'Any, Int'
```

The same goes for assigning it, passing it to a function, or returning it from one. For a list or a map, every element is checked.
A call with an `Any` argument only uses an overload that takes an `Any` there: one whose parameter is `Any`, or has no annotation, which is then an `Any` in its body. A cast picks another:

```ndc
fn describe(x: Int) => "an int";
fn describe(x: Any) => "something";
let a: Any = 5;
print(describe(a), describe(a as Int)); // something an int
```

You can also check it yourself with a [cast](../features/casts.md): `value as List<Int>`.
Functions like `print`, `string` and `==` take a value of any type as it is, so they take an `Any` too.
20 changes: 18 additions & 2 deletions manual/src/reference/types/function.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ let players = [Player("ann", 3), Player("bob", 5)];
players.map(fn(p) => p.size); // error: p is a Player, which has no size
```

A lambda stored in a variable has one type, like any variable, so unlike a function declared with `fn` it can't take an `Int` in one call and a `Float` in the next. A lambda parameter that nothing gives a type is `Any`, and is checked when the program runs.
A lambda stored in a variable has one type, like any variable, so unlike a function declared with `fn` it can't take an `Int` in one call and a `Float` in the next. A lambda parameter that nothing gives a type is `Any`, which its body then has to work with: in `let f = fn(x) => x + 1;` with nothing calling `f`, no `+` takes `x`, so annotate it as `fn(x: Int)`.

## Generic functions

Expand Down Expand Up @@ -258,7 +258,13 @@ shout("a"); // error: No function called '+' found that matches the arguments
// `shout` calls `inc` here, and `inc` needs `x + 1`
```

To take any value whatever its type, annotate the parameter with `Any`: the body then works with it as the program runs.
To take any value whatever its type, annotate the parameter with `Any`: the body then works with an `Any`, which takes a [cast](../../features/casts.md) to be used as anything else. A call with an `Any` argument has an unannotated parameter be an `Any` too, so its body has to work with one:

```ndc,fails
fn inc(x) => x + 1;
let a: Any = 1;
inc(a); // error: No function called '+' found that matches the arguments 'Any, Int'
```

## Function overloading

Expand All @@ -284,6 +290,16 @@ print(describe("a")); // string
print(describe([1])); // something else
```

An argument of type `Any` only goes to an overload that takes an `Any`, whatever it holds as the program runs, and a cast picks another:

```ndc
fn describe(x: Int) { "int" }
fn describe(x: Any) { "something else" }

let a: Any = 1;
print(describe(a), describe(a as Int)); // something else int
```

Two overloads in the same scope can't have the same parameter types. Parameter names don't count:

```ndc,fails
Expand Down
4 changes: 2 additions & 2 deletions manual/src/reference/types/struct.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,14 +144,14 @@ needs parentheses around the member access: `(s.f)()` first evaluates `s.f`
(the getter) and then calls its result.

```ndc
struct Callback { f: Any }
struct Callback { f: Fn(Int) -> Int }
let cb = Callback(fn (a) => a * 2);

print((cb.f)(21)); // 42
```

```ndc,fails
struct Callback { f: Any }
struct Callback { f: Fn(Int) -> Int }
let cb = Callback(fn (a) => a * 2);

cb.f(21); // ERROR: this is method-call syntax for `f(cb, 21)`
Expand Down
2 changes: 1 addition & 1 deletion manual/src/reference/types/tuple.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ let point = ("origin", 0, 0);
print(point[3]); // error: Index 3 is out of bounds for a tuple of 3 elements
```

An index computed as the program runs, like `t[i]`, could pick any element, so it gives `Any`.
An index computed as the program runs, like `t[i]`, could pick any element, so it gives `Any`, which takes a [cast](../../features/casts.md) to say what it is: `t[i] as Int`.

Create a 1-element tuple by adding a trailing comma:

Expand Down
2 changes: 1 addition & 1 deletion manual/src/reference/variables-and-scopes.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ xs.push(1); // xs is List<Int> from here on
xs.push("two"); // error: No function called 'push' found that matches the arguments 'List<Int>, String'
```

When nothing settles them, they stay `Any`.
When nothing settles them, they stay `Any`, which takes a [cast](../features/casts.md) to be used as anything else.

A variable that only gets its real value later holds an `Option`:

Expand Down
45 changes: 25 additions & 20 deletions manual/src/troubleshooting/overload-dispatch-collections.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,35 @@

## Background

Andy C++ picks the overload a call uses before the program runs, from the types of its arguments, so the call costs nothing extra at runtime. Only when an argument has type `Any`, because it was annotated as `Any` or comes from a function that returns `Any`, does the VM pick as the program runs, by testing each candidate overload against the values.
Andy C++ picks the overload a call uses before the program runs, from the types of its arguments, so the call costs nothing extra at runtime. When every argument type is known and no overload can accept them, the call could only ever fail, so it is rejected before the program runs with a `No function called '…' found that matches the arguments` error.

The reverse also holds. When every argument type is known and no overload can accept them, the call could only ever fail at runtime, so it is rejected before the program runs with a `No function called '…' found that matches the arguments` error.
## An `Any` takes a cast

## Element types cost a scan
An argument of type `Any`, because it was annotated as `Any` or comes from a function that returns `Any`, only goes to an overload that takes an `Any`. Two overloads like these take none:

To pick an overload for a value of type `Any`, the VM first looks only at what kind of value it is, which doesn't depend on its size:

| Parameter type | Check performed |
|---|---|
| `List` | Is the value a list? |
| `Map` | Is the value a map? |
| `Deque` | Is the value a deque? |
| `Iterable` | Is the value any sequence type? |
| `String`, `Int`, … | Exact kind check |
```ndc,fails
fn process(xs: List<Int>) => "ints";
fn process(xs: List<String>) => "strings";
let value: Any = [1];
process(value)
// error[analysis]: No function called 'process' found that matches the arguments 'Any'
```

A parameter like `List<Int>` doesn't take a list in that first pass. When no overload takes the arguments without looking at their elements, the VM checks the elements before the call fails, scanning each container. So two overloads like these still work for a value of type `Any`:
A [cast](../features/casts.md) says which one is meant. It checks the value as the program runs, and for a list that scans each element, but the cost lands at a site you wrote:

```ndc
fn process(xs: List<Int>) => "ints";
fn process(xs: List<String>) => "strings";
let value: Any = [1];
print(process(value as List<Int>)); // ints
```
fn process(xs: List<Int>)
fn process(xs: List<String>)
```

Each call through an `Any` scans the list, though, and an overload that takes the value without looking at its elements, like `process(x: Any)`, is picked before either of them.
To find out which one it is, test it with `as?`, which gives `None` when the value isn't one:

```ndc
let value: Any = ["a"];
print(value as? List<Int>, value as? List<String>); // None Some(["a"])
```

## A `List<Any>` fits no typed overload

Expand All @@ -37,7 +42,7 @@ fn sum(Iterable<Float>) -> Float
fn sum(Iterable<Number>) -> Number
```

A value known only as a `List<Any>` fits none of them, as its elements could be anything. Rather than pick as the program runs and scan, the call is rejected before it runs:
A value known only as a `List<Any>` fits none of them, as its elements could be anything, so the call is rejected before it runs:

```ndc,fails
let values: List<Any> = [1, 2, 3];
Expand All @@ -49,7 +54,7 @@ values.sum()

## Workaround

State the element type with a [cast](../features/casts.md). A cast scans the value, but the cost lands at a site you wrote rather than inside every call:
State the element type with a [cast](../features/casts.md):

```ndc
let values: List<Any> = [1, 2, 3];
Expand All @@ -58,4 +63,4 @@ let values: List<Any> = [1, 2, 3];
print((values as List<Int>).sum()); // 6
```

Better still, leave out the `Any`: a value keeps the type it was created with unless it passes through something typed `Any`, and a parameter without an annotation takes the type of each call's argument.
Better still, leave out the `Any`: a value keeps the type it was created with unless it's stored as an `Any`, and a parameter without an annotation takes the type of each call's argument.
4 changes: 2 additions & 2 deletions ndc_analyser/src/analyser/assignments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -229,8 +229,8 @@ impl Analyser {
/// `stored_type` are kept, settling type variables on the way: in
/// `let xs = []; xs[0] = 1`, the write settles the element type of `xs`
/// as `Int`. The stored type never widens to fit the value. An `Any`
/// value fits anywhere, as `Any` is the dynamic type, and is checked at
/// `node` as the program runs.
/// value only fits where an `Any` is kept: it takes a cast to be stored
/// as anything else.
pub(super) fn accepts_write(
&mut self,
stored_type: &StaticType,
Expand Down
2 changes: 2 additions & 0 deletions ndc_analyser/src/analyser/calls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ impl Analyser {
hides_functions,
span,
)
} else if types.contains(&StaticType::Any) {
AnalysisError::function_not_found_for_any(name, types, span)
} else if overloads.a_cast_could_resolve(candidates, types) {
AnalysisError::function_not_found_suggest_cast(name, types, span)
} else {
Expand Down
8 changes: 6 additions & 2 deletions ndc_analyser/src/analyser/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,14 +422,18 @@ impl Analyser {

/// Records what the body of a generic function calls at `call`, which
/// is compiled once for every type it's called with: the overloads that
/// can take the arguments, picked as the program runs.
/// can take the arguments, picked as the program runs. Its type
/// parameters, and the `Any` that a call picked as it runs gives, stand
/// for any type there: an overload that takes an Int may get one. Each
/// call of the function checks its body with the types it's given, so
/// an `Any` the program wrote was checked there.
fn pick_for_any_arguments(&mut self, call: &CallConstraint) {
let arguments = call
.arguments()
.iter()
.map(|argument| {
argument.replace(&|part| {
matches!(part, StaticType::Generic(_)).then_some(StaticType::Any)
matches!(part, StaticType::Any).then(|| StaticType::Generic("T".into()))
Comment on lines 435 to +436

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Specialize generic bodies for actual Any arguments

When a generic wrapper receives an actual Any, its copied constraints now select only the overload accepting Any, but instance_for still reuses the unspecialized body whenever every key type is Any. That body retains the permissive runtime dispatch recorded here. For example, with fn pick(x: Int) => 1; fn pick(x: Any) => "fallback"; fn wrap(x) => pick(x); let a: Any = 1;, wrap(a) is inferred as String but executes the Int overload and returns 1. Besides violating the new overload rule, this lets subsequent statically typed operations receive the wrong runtime type. Actual Any calls need their own specialized picks rather than reusing the generic runtime-dispatched body.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 58993f3: a call with Any arguments now gets its own instance (test 017_dynamic_any/027).

})
})
.collect::<Vec<_>>();
Expand Down
11 changes: 6 additions & 5 deletions ndc_analyser/src/analyser/instances.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,17 +228,18 @@ impl Analyser {
}

/// The instance of `function` for these types of its key: the function
/// itself when they're all `Any`, as its own body picks its calls as the
/// program runs, or when it was declared by an earlier batch, which
/// compiled it already.
/// itself when it was declared by an earlier batch, which compiled it
/// already.
fn instance_for(
&mut self,
function: SymbolId,
key: Vec<StaticType>,
new: &mut Vec<(SymbolId, SymbolId, Vec<StaticType>)>,
) -> SymbolId {
let is_own = key.iter().all(|typ| *typ == StaticType::Any);
if is_own || !self.declared_in_batch.contains(&function) {
// A call with `Any` arguments gets an instance too: its calls take
// only the overloads that take `Any`, while the function's own body
// leaves its picks to the program, for its value.
if !self.declared_in_batch.contains(&function) {
return function;
Comment on lines +242 to 243

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve Any-specific overload picks across REPL batches

When a generic function was declared on an earlier REPL line, this branch still bypasses specialization. For example, evaluate fn pick(x: Int) => 1; fn pick(x: Any) => "fallback"; fn wrap(x) => pick(x); in one batch, then let a: Any = 1; let s: String = wrap(a); print(s); in the next. The copied constraints infer String, but the previously compiled body dynamically selects pick(Int) and returns 1, giving a statically typed variable the wrong runtime type. Although same-batch calls now get an instance, the remaining earlier-batch bypass is fresh evidence that the mismatch persists in the REPL. Earlier-batch calls also need to preserve the overload selection established by their actual argument types.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Real, but it needs later REPL lines to compile instances of earlier lines' functions; tracked in #354.

}
if let Some(&instance) = self.instances.get(&(function, key.clone())) {
Expand Down
Loading
Loading