Skip to content

Commit

Permalink
Support labelled arguments in function calls
Browse files Browse the repository at this point in the history
  • Loading branch information
dusty-phillips committed Sep 16, 2024
1 parent 5b998c6 commit df5cf65
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 45 deletions.
1 change: 1 addition & 0 deletions src/glimpse/error.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ pub type TypeCheckError {
NoSuchModule(name: String)
NotCallable(got: String)
InvalidArguments(expected: String, actual_arguments: String)
InvalidArgumentLabel(expected: String, got: String)
DuplicateCustomType(name: String)
}

Expand Down
88 changes: 60 additions & 28 deletions src/glimpse/internal/typecheck.gleam
Original file line number Diff line number Diff line change
@@ -1,29 +1,62 @@
import glance
import gleam/dict
import gleam/list
import gleam/option
import gleam/result
import glimpse/error
import glimpse/internal/typecheck/environment.{
type Environment, type EnvironmentFold, type EnvironmentResult, type TypeState,
type Environment, type EnvironmentFold, type EnvironmentResult,
type TypeStateFold, type TypeStateResult,
}
import glimpse/internal/typecheck/functions.{
type CallableState, type CallableStateFold, type CallableStateResult,
}
import glimpse/internal/typecheck/types.{type TypeResult}
import glimpse/internal/typecheck/variants.{type VariantField}
import pprint

/// Used when checking the fnction signature.
/// Used when checking the function signature.
/// Ensures that the types in the signature exist in our environment and maps them
// to glimpse Types.

pub fn function_parameter(
environment: Environment,
/// to glimpse Types.
pub fn fold_parameter_into_callable(
state: CallableStateResult,
param: glance.FunctionParameter,
) -> TypeResult {
case param {
glance.FunctionParameter(type_: option.None, ..) ->
todo as "Not inferring function parameters yet (requires generics or Skolem vars)"
glance.FunctionParameter(type_: option.Some(glance_type), ..) ->
type_(environment, glance_type)
) -> CallableStateFold {
case state {
Error(error) -> list.Stop(Error(error))
Ok(functions.CallableState(environment, reversed_by_position, labels)) -> {
case param {
glance.FunctionParameter(type_: option.None, ..) ->
todo as "Not inferring function parameters yet (requires generics or Skolem vars)"

glance.FunctionParameter(
label: option.Some(label),
type_: option.Some(glance_type),
..,
) -> {
use glimpse_type <- result.try(type_(environment, glance_type))
Ok(functions.CallableState(
environment,
[glimpse_type, ..reversed_by_position],
dict.insert(labels, label, reversed_by_position |> list.length),
))
}

glance.FunctionParameter(
label: option.None,
type_: option.Some(glance_type),
..,
) -> {
use glimpse_type <- result.try(type_(environment, glance_type))
Ok(functions.CallableState(
environment,
[glimpse_type, ..reversed_by_position],
labels,
))
}
}
|> list.Continue
}
}
}

Expand All @@ -41,7 +74,7 @@ pub fn fold_function_parameter_into_env(
glance.FunctionParameter(name: glance.Discarded(_), ..) ->
Ok(environment)
glance.FunctionParameter(type_: option.None, ..) ->
todo as "Not inferring untyped parameters yet"
todo as "Not inferring function parameters yet"
glance.FunctionParameter(
name: glance.Named(name),
type_: option.Some(glance_type),
Expand Down Expand Up @@ -217,36 +250,35 @@ pub fn call(
target: glance.Expression,
arguments: List(glance.Field(glance.Expression)),
) -> TypeResult {
let glimpse_arguments_result =
let glimpse_argument_fields_result =
arguments
|> list.map(call_field(environment, _))
|> result.all

use glimpse_target <- result.try(expression(environment, target))
use glimpse_arguments <- result.try(glimpse_arguments_result)
use glimpse_argument_fields <- result.try(glimpse_argument_fields_result)

case glimpse_target {
types.CallableType(target_arguments, target_return)
if glimpse_arguments == target_arguments
-> Ok(target_return)
types.CallableType(..) ->
Error(error.InvalidArguments(
types.to_string(glimpse_target),
"(" <> types.list_to_string(glimpse_arguments) <> ")",
))
types.CallableType(target_arguments, target_labels, target_return) -> {
functions.order_call_arguments(
glimpse_argument_fields,
target_arguments,
target_labels,
)
|> result.replace(target_return)
}
_ -> Error(error.NotCallable(types.to_string(glimpse_target)))
}
}

pub fn call_field(
environment: Environment,
field: glance.Field(glance.Expression),
) -> TypeResult {
) -> error.TypeCheckResult(glance.Field(types.Type)) {
case field {
glance.Field(option.Some(_label), _) ->
todo as "labelled call fields not supported yet"
glance.Field(option.None, arg_expr) -> {
expression(environment, arg_expr)
glance.Field(label_opt, arg_expr) -> {
use type_ <- result.try(expression(environment, arg_expr))
Ok(glance.Field(label_opt, type_))
}
}
}
Expand Down
7 changes: 7 additions & 0 deletions src/glimpse/internal/typecheck/fields.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import glance

/// Helper to extract the item from a Field, ignoring the label
pub fn extract_item(field: glance.Field(a)) -> a {
let glance.Field(_label, item) = field
item
}
148 changes: 148 additions & 0 deletions src/glimpse/internal/typecheck/functions.gleam
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import glance
import gleam/dict
import gleam/list
import gleam/option
import gleam/result
import gleam/string
import glimpse/error
import glimpse/internal/typecheck/environment
import glimpse/internal/typecheck/types.{type Type}

pub type CallableState {
CallableState(
environment: environment.Environment,
reversed_by_position: List(Type),
labels: dict.Dict(String, Int),
)
}

pub type CallableStateResult =
error.TypeCheckResult(CallableState)

pub type CallableStateFold =
error.TypeCheckFold(CallableState)

pub fn empty_state(environment: environment.Environment) -> CallableState {
CallableState(environment, [], dict.new())
}

pub fn to_callable_type(state: CallableState, return_type: Type) -> Type {
types.CallableType(
state.reversed_by_position |> list.reverse,
state.labels,
return_type,
)
}

type OrderedFoldState {
OrderedFoldState(
reversed_ordered: List(Type),
positional_remaining: List(Type),
)
}

/// Confirm that a function called with `called_with` can safely call
/// a function with the provided target_argument_types and position_labels.
///
/// These probably came from a match on types.Callable
///
/// Returns an error if:
/// * arity of called_with doesn't match target_argument_types
/// * called_with includes labelled arguments that are not in target_argument_types
/// * the types after mapping labels to positions do not match
pub fn order_call_arguments(
called_with: List(glance.Field(Type)),
target_argument_types: List(Type),
position_labels: dict.Dict(String, Int),
) -> error.TypeCheckResult(List(Type)) {
let #(positional_called_with, labelled_called_with) =
split_fields_by_type(called_with)

use called_with_types_by_position <- result.try(labels_to_position_dict(
labelled_called_with,
position_labels,
))

let target_argument_types_result =
list.index_fold(
called_with,
Ok(OrderedFoldState([], positional_called_with)),
fn(state, _, index) {
case state, dict.get(called_with_types_by_position, index) {
Error(error), _ -> Error(error)
Ok(OrderedFoldState(reversed_ordered, positional)), Ok(type_) ->
Ok(OrderedFoldState([type_, ..reversed_ordered], positional))
Ok(OrderedFoldState(reversed_ordered, [head, ..rest])), Error(_) ->
Ok(OrderedFoldState([head, ..reversed_ordered], rest))
Ok(OrderedFoldState(reversed_ordered, [])), Error(_) ->
Error(argument_error(target_argument_types, reversed_ordered))
}
},
)
|> result.map(fn(state) { state.reversed_ordered |> list.reverse })

use positioned_argument_types <- result.try(target_argument_types_result)

case positioned_argument_types == target_argument_types {
True -> Ok(positioned_argument_types)
False ->
Error(argument_error(target_argument_types, positioned_argument_types))
}
}

fn split_fields_by_type(
fields: List(glance.Field(Type)),
) -> #(List(Type), dict.Dict(String, Type)) {
let #(reversed_positional, labelled) =
list.fold(fields, #([], dict.new()), fn(state, field) {
let #(reversed_positional, labelled) = state
case field {
glance.Field(option.None, type_) -> #(
[type_, ..reversed_positional],
labelled,
)
glance.Field(option.Some(label), type_) -> #(
reversed_positional,
dict.insert(labelled, label, type_),
)
}
})

#(list.reverse(reversed_positional), labelled)
}

/// Given the dict of labeled args and their associated types
/// and a dict of what positions labels are expected to go at,
/// construct a dict mapping positions to types
///
/// Error if a label in the call site is not used in the destination
fn labels_to_position_dict(
called_with_labels: dict.Dict(String, Type),
target_label_positions: dict.Dict(String, Int),
) -> error.TypeCheckResult(dict.Dict(Int, Type)) {
called_with_labels
|> dict.to_list
|> list.map(fn(tuple) {
let #(label, type_) = tuple
dict.get(target_label_positions, label)
|> result.map(fn(position) { #(position, type_) })
|> result.map_error(fn(_) {
error.InvalidArgumentLabel(
"(" <> target_label_positions |> dict.keys() |> string.join(", ") <> ")",
label,
)
})
})
|> result.all
|> result.map(dict.from_list)
}

fn argument_error(
expected: List(Type),
actual: List(Type),
) -> error.TypeCheckError {
error.InvalidArguments(
"(" <> types.list_to_string(expected) <> ")",
"(" <> types.list_to_string(actual) <> ")",
)
}
13 changes: 10 additions & 3 deletions src/glimpse/internal/typecheck/types.gleam
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import glance
import gleam/dict.{type Dict}
import gleam/iterator
import gleam/list
import gleam/option
Expand All @@ -12,7 +13,13 @@ pub type Type {
StringType
BoolType
CustomType(name: String)
CallableType(parameters: List(Type), return: Type)
CallableType(
/// All parameters (labelled or otherwise)
parameters: List(Type),
/// Map of label to its position in parameters list
position_labels: Dict(String, Int),
return: Type,
)
}

pub type TypeResult =
Expand All @@ -26,7 +33,7 @@ pub fn to_string(type_: Type) -> String {
StringType -> "String"
BoolType -> "Bool"
CustomType(name) -> name
CallableType(parameters, return) ->
CallableType(parameters, _labels, return) ->
string_builder.from_string("fn (")
|> string_builder.append(list_to_string(parameters))
|> string_builder.append(") -> ")
Expand Down Expand Up @@ -54,7 +61,7 @@ pub fn to_glance(type_: Type) -> glance.Type {
BoolType -> glance.NamedType("Bool", option.None, [])
// TODO: CustomType will need a module field
CustomType(name) -> glance.NamedType(name, option.None, [])
CallableType(parameters, return) ->
CallableType(parameters, _labels, return) ->
glance.FunctionType(list.map(parameters, to_glance), to_glance(return))
}
}
Expand Down
15 changes: 10 additions & 5 deletions src/glimpse/typecheck.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import glimpse/internal/typecheck as intern
import glimpse/internal/typecheck/environment.{
type Environment, type EnvironmentFold, type EnvironmentResult,
}
import glimpse/internal/typecheck/functions
import glimpse/internal/typecheck/types

/// Infer and typecheck a single module in the given package. Any modules that
Expand Down Expand Up @@ -96,7 +97,8 @@ pub fn custom_type(
}

/// Given a glance function signature, inject that signature into the environment definitions as
/// a callable type. The body is not typechecked yet.
/// a callable type. The body is not typechecked at this point.
/// TODO: Inferring function parameter types
pub fn function_signature(
state: EnvironmentResult,
function: glance.Function,
Expand All @@ -105,11 +107,14 @@ pub fn function_signature(
Error(error) -> list.Stop(Error(error))
Ok(environment) ->
{
use params <- result.try(
use param_state <- result.try(
function.parameters
|> list.map(intern.function_parameter(environment, _))
|> result.all,
|> list.fold_until(
Ok(functions.empty_state(environment)),
intern.fold_parameter_into_callable,
),
)

case function.return {
option.None -> todo as "not inferring return values yet"
option.Some(glance_return_type) -> {
Expand All @@ -121,7 +126,7 @@ pub fn function_signature(
environment
|> environment.add_def(
function.name,
types.CallableType(params, return),
functions.to_callable_type(param_state, return),
),
)
}
Expand Down
Loading

0 comments on commit df5cf65

Please sign in to comment.