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
23 changes: 1 addition & 22 deletions ndc_analyser/src/analyser/assignments.rs
Original file line number Diff line number Diff line change
Expand Up @@ -296,30 +296,9 @@ impl Analyser {
{
self.check_write(stored_type, value_type, span, Some(lvalue.id));
}
AssignmentTarget::Member { .. } => {
AssignmentTarget::Member { .. } | AssignmentTarget::Index { .. } => {
self.check_write(stored_type, value_type, span, Some(lvalue.id));
}
AssignmentTarget::Index { index, .. } => {
if self.accepts_write(stored_type, value_type, Some(lvalue.id)) {
return;
}
// `xs[0..2] = (1, 2)` stores the tuple's elements.
let is_slice = self
.result
.analysis
.expr_types
.get(&index.id)
.is_some_and(Self::index_type_is_slice);
if is_slice
&& let Some(stored_element) =
self.vars.resolve(stored_type).index_element_type()
&& let Some(value_element) = value_type.sequence_element_type()
&& self.accepts_write(&stored_element, &value_element, None)
{
return;
}
self.emit_rejected_write(stored_type, value_type, span);
}
// Each target gets the element unpacked for it, as in a `let`.
AssignmentTarget::Sequence(targets) => {
let StaticType::Tuple(stored_types) = stored_type else {
Expand Down
21 changes: 6 additions & 15 deletions ndc_analyser/src/analyser/calls.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use super::Analyser;
use super::solver::{CallNodes, CallSite, Constraint};
use super::solver::{CallNodes, CallSite};
use crate::error::AnalysisError;
use crate::overloads::{CallKind, Overloads};
use itertools::Itertools;
Expand Down Expand Up @@ -124,20 +124,11 @@ impl Analyser {
..
} => {
if let Some((typ, parameters)) = self.bind_signature(&callee, &arguments) {
for (node, expected) in
self.expected_of_any(&parameters, &arguments, &nodes)
{
self.expect(node, expected);
}
for (sequence, element) in
self.iterations_waited_for(&parameters, &arguments)
{
self.wait(Constraint::Iterate {
sequence,
element,
span,
});
}
let checks = self.expected_of_any(&parameters, &arguments, &nodes);
self.pending.checks.extend(checks);
let iterations = self.iterations_waited_for(&parameters, &arguments);
self.pending.iterations.extend(iterations);
self.flush_copies(None, span);
typ
} else {
let arguments = arguments
Expand Down
5 changes: 4 additions & 1 deletion ndc_analyser/src/analyser/declarations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub(super) struct FunctionDeclaration<'a> {
pub parameters: &'a [FunctionParameter],
pub body: &'a ExpressionLocation,
pub return_annotation: Option<&'a TypeExpr>,
/// Whether it's a `pure fn`, whose results are cached.
pub pure: bool,
pub span: Span,
}

Expand All @@ -40,6 +42,7 @@ impl Analyser {
parameters,
body,
return_annotation,
pure,
span,
} = function;
let declared = name.map(|name| (name, self.resolver.declared_symbol(id)));
Expand Down Expand Up @@ -164,7 +167,7 @@ impl Analyser {
self.vars.leave_level();
self.declaring = None;
self.declared_in_batch.insert(symbol);
let typ = self.generalize(symbol, &signature, id, [first_use, first_check]);
let typ = self.generalize(symbol, &signature, id, pure, [first_use, first_check]);
self.within_size_limit(symbol, name, typ, span)
}
_ => signature,
Expand Down
12 changes: 12 additions & 0 deletions ndc_analyser/src/analyser/generics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ impl Analyser {
symbol: SymbolId,
signature: &StaticType,
declaration: NodeId,
pure: bool,
[first_use, first_check]: [usize; 2],
) -> StaticType {
self.solve_waiting();
Expand Down Expand Up @@ -157,12 +158,23 @@ impl Analyser {
// An instance is for the types of the type parameters that what it
// compiles differently depends on: the constraints and uses it
// picks for, and the checks.
// A `pure fn` caches its results by its arguments' values, which can
// be equal across types, as `1` and `1.0` are, so each set of types
// gets an instance, and a cache, of its own.
let parameters = match self.vars.resolve(signature) {
StaticType::Function {
parameters: Some(parameters),
..
} if pure => parameters,
_ => Vec::new(),
};
let mut key: Vec<Box<str>> = Vec::new();
for typ in scheme
.iter()
.flat_map(|waiting| waiting.constraint.inputs())
.chain(uses.iter().flat_map(|generic_use| &generic_use.key))
.chain(checks.iter().map(|(_, _, expected)| expected))
.chain(&parameters)
{
for name in typ.type_parameters() {
if !key.contains(&name) {
Expand Down
12 changes: 7 additions & 5 deletions ndc_analyser/src/analyser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -489,13 +489,15 @@ impl Analyser {
parameters,
body,
return_annotation,
pure,
..
} => self.analyse_function_declaration(FunctionDeclaration {
id: *id,
name: name.as_deref(),
parameters,
body,
return_annotation: return_annotation.as_ref(),
pure: *pure,
span: *span,
}),
Expression::Block { statements } => {
Expand Down Expand Up @@ -667,11 +669,11 @@ impl Analyser {
}
Expression::RangeInclusive { start, end }
| Expression::RangeExclusive { start, end } => {
if let Some(start) = start {
self.analyse_expr(start);
}
if let Some(end) = end {
self.analyse_expr(end);
// A range counts in Ints, so its bounds are Ints: in `0..n`,
// `n` is one, whatever else it's used for.
for bound in [start, end].into_iter().flatten() {
let typ = self.analyse_expr(bound);
self.check_write(&StaticType::Int, &typ, bound.span, Some(bound.id));
}

StaticType::Iterator(Box::new(StaticType::Int))
Expand Down
49 changes: 47 additions & 2 deletions ndc_analyser/src/analyser/solver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,26 @@ impl Analyser {
.flat_map(|argument| self.vars.unsettled(argument))
.filter(|&var| self.vars.is_literal(var))
.collect::<Vec<_>>();
// The nearest scope with an overload that takes the call hides those
// further out. When that scope has one overload, whose parameters
// take whatever the literals turn out to be, as a nested `fn d(x)`
// does, the call is its, though a literal isn't decided yet.
if !literals.is_empty()
&& !Self::operand_could_be_a_tuple(kind, arguments)
&& let Some(nearest) = self
.overloads()
.reachable_groups(candidates)
.into_iter()
.find(|group| {
group
.iter()
.any(|&symbol| self.could_accept(symbol, arguments))
})
&& let [only] = nearest[..]
&& self.takes_any_literal(only, arguments)
{
return EarlyOverload::Only(only);
Comment on lines +1374 to +1377

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Wait for nonliteral arguments before committing to a scope

When another argument is an unresolved nonliteral type variable, takes_any_literal does not establish that this overload will still accept the call once that argument is known. For example, fn d(x: Int, y: String) => "outer"; fn run(y) { fn d(x, y: Int) => "inner"; d(1, y) } print(run("s")); now commits to the inner d while analysing run, forcing y to Int and rejecting the String call. Previously, both scopes remained viable until the call supplied String, allowing the outer overload to be selected. Keep this call undecided unless the nonliteral arguments also cannot change which scope accepts it.

Useful? React with 👍 / 👎.

}
let viable = reachable
.iter()
.copied()
Expand Down Expand Up @@ -1396,6 +1416,31 @@ impl Analyser {
}
}

/// Whether `symbol`'s parameter for each argument with a literal in it
/// takes any type, as an unannotated one does, and the other arguments
/// are known, so nothing still to come can change whether `symbol` takes
/// the call.
fn takes_any_literal(&self, symbol: SymbolId, arguments: &[StaticType]) -> bool {
let Some(parameters) = self.overloads().parameters_of(symbol) else {
return false;
};
parameters.len() == arguments.len()
&& parameters
.iter()
.zip(arguments)
.all(|(parameter, argument)| {
let unsettled = self.vars.unsettled(argument);
if unsettled.iter().all(|&var| !self.vars.is_literal(var)) {
return unsettled.is_empty();
}
match self.vars.resolve(parameter) {
StaticType::Var(var) => !self.vars.is_literal(var),
StaticType::Generic(_) => true,
_ => false,
}
})
}

/// Whether one of `reachable` other than `except` takes `arguments`
/// once each literal among them is `Any`.
fn fits_once_any(
Expand Down Expand Up @@ -1955,8 +2000,8 @@ impl Analyser {
.zip(arguments)
.all(|(parameter, argument)| match (parameter, argument) {
(StaticType::Any, argument) => *argument == StaticType::Any,
(StaticType::Iterable(_), StaticType::Var(var)) => {
self.vars.is_literal(*var)
(StaticType::Iterable(_), argument) => {
!self.vars.waits_to_iterate(argument)
}
_ => true,
})
Expand Down
36 changes: 8 additions & 28 deletions ndc_analyser/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,15 +135,7 @@ impl TypeVars {
/// A literal whose type the literal type `var` holds values of and that
/// isn't settled yet, as `1`'s is in `[1]`.
pub(crate) fn held_literal(&self, var: TypeVar) -> Option<TypeVar> {
self.bounds[var.0 as usize]
.iter()
.find_map(|bound| match bound {
Bound::Holds(held, _, _) => match self.resolve(held) {
StaticType::Var(held) if held != var && self.is_literal(held) => Some(held),
_ => None,
},
Bound::Number(..) => None,
})
self.held_literals(var).into_iter().next()
}

/// The unsettled literal types that each unsettled literal type is
Expand Down Expand Up @@ -366,28 +358,13 @@ impl TypeVars {
return true;
};
let snapshot = self.snapshot();
let fits = defaults.iter().any(|default| self.try_settle(var, default));
let fits = defaults
.iter()
.any(|default| self.try_unify(&StaticType::Var(var), default));
self.rollback(snapshot);
fits
}

/// Settles `var` as `typ` if `typ` is within its bounds, and returns
/// whether it did. A variable can't stand for a type that contains it.
fn try_settle(&mut self, var: TypeVar, typ: &StaticType) -> bool {
if self
.resolve(typ)
.contains(&|part| *part == StaticType::Var(var))
{
return false;
}
let snapshot = self.snapshot();
let settled = self.settle(var, typ.clone());
if !settled {
self.rollback(snapshot);
}
settled
}

/// What the unsettled literal type `var` is shown as: what it would be
/// if nothing said otherwise, as far as that's known.
pub(crate) fn preview(&self, var: TypeVar) -> StaticType {
Expand Down Expand Up @@ -641,7 +618,10 @@ impl TypeVars {
Some(defaults) if !self.bounds[var.0 as usize].is_empty() => defaults,
_ => vec![self.fallbacks[var.0 as usize].clone()],
};
if !defaults.iter().any(|default| self.try_settle(var, default)) {
if !defaults
.iter()
.any(|default| self.try_unify(&StaticType::Var(var), default))
{
// `Any` takes anything a literal holds.
self.settle(var, StaticType::Any);
}
Expand Down
19 changes: 0 additions & 19 deletions ndc_core/src/static_type.rs
Original file line number Diff line number Diff line change
Expand Up @@ -570,25 +570,6 @@ impl StaticType {
}
}

/// Returns a new type with the element type replaced. For container types
/// like `List<T>`, this returns `List<new_elem>`. Returns `None` if the
/// type does not have a replaceable element type.
pub fn with_element_type(&self, new_elem: Self) -> Self {
match self {
Self::List(_) => Self::List(Box::new(new_elem)),
Self::Iterable(_) => Self::Iterable(Box::new(new_elem)),
Self::Iterator(_) => Self::Iterator(Box::new(new_elem)),
Self::MinHeap(_) => Self::MinHeap(Box::new(new_elem)),
Self::MaxHeap(_) => Self::MaxHeap(Box::new(new_elem)),
Self::Deque(_) => Self::Deque(Box::new(new_elem)),
Self::Map { key, .. } => Self::Map {
key: key.clone(),
value: Box::new(new_elem),
},
_ => self.clone(),
}
}

pub fn index_element_type(&self) -> Option<Self> {
if let Self::Map { value, .. } = self {
return Some(value.as_ref().clone());
Expand Down
29 changes: 15 additions & 14 deletions ndc_stdlib/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -455,21 +455,22 @@ fn vm_set_at_index(container: &Value, index_value: &Value, rhs: Value) -> Result
list[idx] = rhs;
}
VmOffset::Range(from, to) => {
let rhs_vec = match rhs {
Value::Object(o) => match o.as_ref() {
Object::List(l) => l.borrow().clone(),
Object::Tuple(t) => t.clone(),
_ => {
return Err(VmError::native(
"cannot assign non-list to list slice",
));
}
},
_ => {
return Err(VmError::native(
"cannot assign non-list to list slice",
));
// Any sequence goes in a slice: a list, a tuple, a
// range or another iterator.
let rhs_vec = match &rhs {
Value::Object(o) if matches!(o.as_ref(), Object::List(_)) => {
let Object::List(l) = o.as_ref() else {
unreachable!("matched a list");
};
l.borrow().clone()
}
_ => rhs
.clone()
.try_into_iter()
.ok_or_else(|| {
VmError::native("cannot assign non-sequence to list slice")
})?
.collect(),
};
let tail: Vec<Value> = list.drain(from..).collect();
list.extend(rhs_vec);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// The nearest scope with a function that takes a call is the call's, so a
// later call with another type can't hand an earlier one to an outer `d`.
// expect-output: inner inner
fn d(x: Int) => "outer";
fn run() { fn d(x) => "inner"; d(1) }
fn run2() { fn d(x) => "inner"; let r = d(1); d(2.5); r }
print(run(), run2());
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// A call whose other arguments aren't known yet waits for them before the
// nearest scope takes it: `y` may make the outer `d` the one that fits.
// expect-output: outer
fn d(x: Int, y: String) => "outer";
fn run(y) {
fn d(x, y: Int) => "inner";
d(1, y)
}
print(run("s"));
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Every non-Int bound used to claim the integer was too large. Only an
// integer that genuinely does not fit is a size problem.
// expect-error: range bounds must be Int, got Number
// Every non-Int bound used to claim the integer was too large. A range
// counts in Ints, so a Number bound is reported before the program runs.
// expect-error: mismatched types: found Number but expected Int
for i in 0n..3n { print(i); }
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// A range counts in Ints, so a literal used as its bound is an Int, and a
// list mixing it with a Float holds `Any`s.
// expect-output: 3 [0.5,3]
let n = 3;
let xs = [0.5, n];
let total = 0;
for i in 0..n {
total += i;
}
print(total, xs);
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// A list slice can be assigned any sequence of its element type.
// expect-output: [1,5,6] [8,9,3]
let xs = [1, 2, 3];
xs[1..3] = 5..7;
let ys = [1, 2, 3];
ys[0..2] = (8, 9);
print(xs, ys);
Loading
Loading