Packages

Infers types for expressions, effects and scope variables of EYG programs.

Current section

Files

Jump to
eyg_analysis src eyg analysis inference levels_j contextual.gleam
Raw

src/eyg/analysis/inference/levels_j/contextual.gleam

import eyg/analysis/type_/binding
import eyg/analysis/type_/binding/error
import eyg/analysis/type_/binding/unify
import eyg/analysis/type_/isomorphic as t
import eyg/ir/dag_json
import eyg/ir/tree as ir
import gleam/dict.{type Dict}
import gleam/list
import gleam/result.{try}
import gleam/set
import multiformats/cid/v1
// None of context is tested
pub type Context {
Context(
env: List(#(String, binding.Poly)),
eff: binding.Mono,
refs: Dict(v1.Cid, binding.Poly),
level: Int,
bindings: Dict(Int, binding.Binding),
)
}
/// pure creates a new inference context to infer an expression with no effects.
/// Any effect from the expression will be a type error
pub fn pure() {
let bindings = new_state()
Context([], t.Empty, dict.new(), 1, bindings)
}
/// unpure creates a new inference context which accepts any effect.
pub fn unpure() {
let bindings = new_state()
let #(t, bindings) = binding.mono(1, bindings)
Context([], t, dict.new(), 1, bindings)
}
/// Pass in a dictionary of reference to types.
/// This dictionary is also used to look up the types of releases by there cid.
pub fn with_references(context, refs) {
Context(..context, refs:)
}
pub fn with_effect(context, label, lift, lower) {
let Context(eff:, ..) = context
let eff = t.EffectExtend(label, #(lift, lower), eff)
Context(..context, eff:)
}
pub fn with_effects(context: Context, effects) {
list.fold_right(effects, context, fn(context, effect) {
let #(label, #(lift, lower)) = effect
with_effect(context, label, lift, lower)
})
}
pub type Analysis(meta) {
Analysis(
bindings: Dict(Int, binding.Binding),
tree: ir.Node(
#(
Result(Nil, error.Reason),
binding.Mono,
binding.Mono,
List(#(String, binding.Poly)),
),
),
original: ir.Node(meta),
)
}
pub fn check(context, source) -> Analysis(_) {
let Context(env:, eff:, refs:, level:, bindings:) = context
let #(bindings, _type, _eff, tree) =
do_infer(source, env, eff, refs, level, bindings)
// TODO make opaque analysis
Analysis(bindings:, tree:, original: source)
}
pub fn missing_references(inference) {
error.missing_references(all_errors(inference))
}
pub fn all_errors(inference) {
let Analysis(_bindings, acc, source) = inference
let meta = ir.get_annotation(source)
let info = ir.get_annotation(acc)
let assert Ok(info) = list.strict_zip(meta, info)
info
|> list.filter_map(fn(pair) {
let #(meta, #(result, _type, _eff, _scope)) = pair
case result {
Ok(Nil) -> Error(Nil)
Error(reason) -> Ok(#(meta, reason))
}
})
}
fn info_at(inference: Analysis(_), desired) {
let Analysis(_bindings, acc, source) = inference
let meta = ir.get_annotation(source)
let info = ir.get_annotation(acc)
let assert Ok(info) = list.strict_zip(meta, info)
info
|> list.find_map(fn(pair) {
let #(meta, info) = pair
case meta == desired {
True -> Ok(info)
False -> Error(Nil)
}
})
}
pub fn type_at(inference: Analysis(_), desired) {
use #(_result, type_, _effect, _scope) <- try(info_at(inference, desired))
Ok(binding.resolve(type_, inference.bindings))
}
pub fn scope_at(inference: Analysis(_), desired) {
use #(_result, _type, _effect, scope) <- try(info_at(inference, desired))
Ok(scope)
}
pub fn arity_at(inference: Analysis(_), desired) {
use type_ <- try(type_at(inference, desired))
count_args(type_)
}
pub fn count_args(type_) {
case type_ {
t.Fun(_, _, return) -> Ok(do_count_args(return, 1))
_ -> Error(Nil)
}
}
fn do_count_args(type_, acc) {
case type_ {
t.Fun(_, _, return) -> do_count_args(return, acc + 1)
_ -> acc
}
}
/// Returns the top type from the analysis over an expression
pub fn type_(inference) {
let Analysis(bindings, acc, _source) = inference
let #(_tree, #(_error, type_, _eff, _env)) = acc
binding.resolve(type_, bindings)
}
pub fn poly_type(inference) {
let Analysis(bindings, acc, _source) = inference
let #(_tree, #(_error, type_, _eff, _env)) = acc
let mono = binding.resolve(type_, bindings)
binding.gen(mono, 1, bindings)
}
// --- old direct code
pub fn new_state() {
dict.new()
}
pub fn infer(source, eff, refs, level, bindings) {
let source = source
let #(bindings, _type, _eff, acc) =
do_infer(source, [], eff, refs, level, bindings)
#(acc, bindings)
}
fn open_effect(eff, level, bindings) {
case eff {
t.Empty -> binding.mono(level, bindings)
t.EffectExtend(label, type_, eff) -> {
let #(eff, bindings) = open_effect(eff, level, bindings)
#(t.EffectExtend(label, type_, eff), bindings)
}
other -> #(other, bindings)
}
}
fn open(type_, level, bindings) {
case type_ {
t.Fun(args, eff, ret) -> {
let #(eff, bindings) = open_effect(eff, level, bindings)
let #(ret, bindings) = open(ret, level, bindings)
#(t.Fun(args, eff, ret), bindings)
}
other -> #(other, bindings)
}
}
pub fn ftv(type_) {
case type_ {
t.Var(x) -> set.from_list([x])
t.Fun(arg, eff, ret) -> set.union(ftv(arg), set.union(ftv(eff), ftv(ret)))
t.Integer | t.Binary | t.String -> set.new()
t.List(el) -> ftv(el)
t.Record(rows) -> ftv(rows)
t.Union(inner) -> ftv(inner)
t.Empty -> set.new()
t.RowExtend(_, field, tail) -> set.union(ftv(field), ftv(tail))
t.EffectExtend(_, #(lift, reply), tail) ->
set.union(ftv(lift), set.union(ftv(reply), ftv(tail)))
t.Never -> set.new()
t.Promise(inner) -> ftv(inner)
}
}
fn close(type_, level, bindings) {
case binding.resolve(type_, bindings) {
t.Fun(arg, eff, ret) -> {
let eff = close_eff(arg, eff, ret, level, bindings)
t.Fun(arg, eff, close(ret, level, bindings))
}
_ -> type_
}
}
fn close_eff(arg, eff, ret, level, bindings) {
let #(last, mapped) = eff_tail(eff)
case last {
Ok(i) -> {
// can only close if would also generalise
let assert Ok(binding.Unbound(l)) = dict.get(bindings, i)
case !set.contains(set.union(ftv(arg), ftv(ret)), i) && l > level {
True -> mapped
False -> eff
}
}
Error(Nil) -> eff
}
}
fn eff_tail(eff) {
case eff {
t.Var(x) -> #(Ok(x), t.Empty)
t.EffectExtend(l, f, tail) -> {
let #(result, tail) = eff_tail(tail)
#(result, t.EffectExtend(l, f, tail))
}
_ -> #(Error(Nil), eff)
}
}
pub type Env =
List(#(String, binding.Poly))
pub fn do_infer(source, env, eff, refs: dict.Dict(_, _), level, bindings) {
let #(exp, _meta) = source
case exp {
ir.Variable(x) ->
case list.key_find(env, x) {
Ok(scheme) -> {
let #(type_, bindings) = binding.instantiate(scheme, level, bindings)
let #(type_, bindings) = open(type_, level, bindings)
let meta = #(Ok(Nil), type_, t.Empty, env)
#(bindings, type_, eff, #(ir.Variable(x), meta))
}
Error(Nil) -> {
let #(type_, bindings) = binding.mono(level, bindings)
let meta = #(Error(error.MissingVariable(x)), type_, t.Empty, env)
#(bindings, type_, eff, #(ir.Variable(x), meta))
}
}
ir.Lambda(x, body) -> {
let #(type_x, bindings) = binding.mono(level, bindings)
let assert t.Var(i) = type_x
let scheme_x = t.Var(#(False, i))
let level = level + 1
let #(type_eff, bindings) = binding.mono(level, bindings)
let #(bindings, type_r, type_eff, inner) =
do_infer(body, [#(x, scheme_x), ..env], type_eff, refs, level, bindings)
let type_ = t.Fun(type_x, type_eff, type_r)
let level = level - 1
let record = close(type_, level, bindings)
let meta = #(Ok(Nil), record, t.Empty, env)
#(bindings, type_, eff, #(ir.Lambda(x, inner), meta))
}
ir.Apply(fun, arg) -> {
// Effects are passed to inner infer because they are effect for creating evaluatin the func and arg,
// not the effect of applying them
let level = level + 1
let #(bindings, ty_fun, eff, fun) =
do_infer(fun, env, eff, refs, level, bindings)
let #(bindings, ty_arg, eff, arg) =
do_infer(arg, env, eff, refs, level, bindings)
let #(ty_ret, bindings) = binding.mono(level, bindings)
let #(test_eff, bindings) = binding.mono(level, bindings)
let #(bindings, result) = case
unify.unify(t.Fun(ty_arg, test_eff, ty_ret), ty_fun, level, bindings)
{
Ok(bindings) -> #(bindings, Ok(Nil))
Error(reason) -> #(bindings, Error(reason))
}
let level = level - 1
// At this point we just check that the effects would generalise a level up.
// It doesn't matter if the eftects are in arg because we apply the arg here
// so we're only interested in final effects
let #(last, mapped) = eff_tail(binding.resolve(test_eff, bindings))
let raised = case last {
Error(Nil) -> test_eff
Ok(i) -> {
let assert Ok(binding) = dict.get(bindings, i)
let level = level - 1
case binding {
binding.Unbound(l) if l > level -> {
mapped
}
_ -> test_eff
}
}
}
let #(bindings, result) = case
unify.unify(test_eff, eff, level, bindings)
{
Ok(bindings) -> #(bindings, result)
// First error, is there a result.and function
Error(reason) -> #(bindings, case result {
Ok(Nil) -> Error(reason)
Error(reason) -> Error(reason)
})
}
let record = close(ty_ret, level, bindings)
let meta = #(result, record, raised, env)
// This returns the raised effect even if error
#(bindings, ty_ret, eff, #(ir.Apply(fun, arg), meta))
}
ir.Let(label, value, then) -> {
let level = level + 1
let #(bindings, ty_value, eff, value) =
do_infer(value, env, eff, refs, level, bindings)
let level = level - 1
let sch_value =
binding.gen(close(ty_value, level, bindings), level, bindings)
let #(bindings, ty_then, eff, then) =
do_infer(then, [#(label, sch_value), ..env], eff, refs, level, bindings)
let meta = #(Ok(Nil), ty_then, t.Empty, env)
#(bindings, ty_then, eff, #(ir.Let(label, value, then), meta))
}
ir.Vacant -> {
let #(type_, bindings) = binding.mono(level, bindings)
let meta = #(Error(error.Todo), type_, t.Empty, env)
#(bindings, type_, eff, #(ir.Vacant, meta))
}
ir.Integer(value) ->
prim(t.Integer, env, eff, level, bindings, ir.Integer(value))
ir.Binary(value) ->
prim(t.Binary, env, eff, level, bindings, ir.Binary(value))
ir.String(value) ->
prim(t.String, env, eff, level, bindings, ir.String(value))
ir.Tail -> prim(t.List(q(0)), env, eff, level, bindings, ir.Tail)
ir.Cons -> prim(cons(), env, eff, level, bindings, ir.Cons)
ir.Empty -> prim(t.Record(t.Empty), env, eff, level, bindings, ir.Empty)
ir.Extend(label) ->
prim(extend(label), env, eff, level, bindings, ir.Extend(label))
ir.Overwrite(label) ->
prim(overwrite(label), env, eff, level, bindings, ir.Overwrite(label))
ir.Select(label) ->
prim(select(label), env, eff, level, bindings, ir.Select(label))
ir.Tag(label) -> prim(tag(label), env, eff, level, bindings, ir.Tag(label))
ir.Case(label) ->
prim(case_(label), env, eff, level, bindings, ir.Case(label))
ir.NoCases -> prim(nocases(), env, eff, level, bindings, ir.NoCases)
ir.Perform(label) ->
prim(perform(label), env, eff, level, bindings, ir.Perform(label))
ir.Handle(label) ->
prim(handle(label), env, eff, level, bindings, ir.Handle(label))
ir.Builtin(id) ->
case builtin(id) {
Ok(poly) -> prim(poly, env, eff, level, bindings, ir.Builtin(id))
Error(Nil) -> {
let #(type_, bindings) = binding.mono(level, bindings)
let meta = #(Error(error.MissingBuiltin(id)), type_, t.Empty, env)
#(bindings, type_, eff, #(ir.Builtin(id), meta))
}
}
ir.ContentReference(cid) ->
lookup_ref(
refs,
error.MissingReference(cid),
cid,
env,
eff,
level,
bindings,
)
ir.ReleaseReference(package, release, cid) ->
lookup_ref(
refs,
error.UndefinedRelease(package, release, cid),
cid,
env,
eff,
level,
bindings,
)
ir.RelativeReference(location:) -> {
// TODO this should get a error of it's own and own lookup path
let cid = dag_json.vacant_cid
lookup_ref(
refs,
error.UndefinedRelease(location, 0, cid),
cid,
env,
eff,
level,
bindings,
)
}
}
}
fn lookup_ref(refs, reason, id, env, eff, level, bindings) {
case dict.get(refs, id) {
Ok(poly) -> prim(poly, env, eff, level, bindings, ir.ContentReference(id))
Error(Nil) -> {
let #(type_, bindings) = binding.mono(level, bindings)
let meta = #(Error(reason), type_, t.Empty, env)
#(bindings, type_, eff, #(ir.ContentReference(id), meta))
}
}
}
fn prim(scheme, env, eff, level, bindings, exp) {
let #(type_, bindings) = binding.instantiate(scheme, level, bindings)
let #(t, bindings) = open(type_, level, bindings)
let meta = #(Ok(Nil), type_, t.Empty, env)
#(bindings, t, eff, #(exp, meta))
}
fn pure1(arg1, ret) {
t.Fun(arg1, t.Empty, ret)
}
fn pure2(arg1, arg2, ret) {
t.Fun(arg1, t.Empty, t.Fun(arg2, t.Empty, ret))
}
fn pure3(arg1, arg2, arg3, ret) {
t.Fun(arg1, t.Empty, t.Fun(arg2, t.Empty, t.Fun(arg3, t.Empty, ret)))
}
// q for quantified
pub fn q(i) {
t.Var(#(True, i))
}
fn cons() {
pure2(q(0), t.List(q(0)), t.List(q(0)))
}
fn extend(l) {
pure2(q(0), t.Record(q(1)), t.Record(t.RowExtend(l, q(0), q(1))))
}
fn overwrite(l) {
pure2(
q(0),
t.Record(t.RowExtend(l, q(1), q(2))),
t.Record(t.RowExtend(l, q(0), q(2))),
)
}
fn select(l) {
pure1(t.Record(t.RowExtend(l, q(0), q(1))), q(0))
}
fn tag(l) {
pure1(q(0), t.Union(t.RowExtend(l, q(0), q(1))))
}
pub fn case_(label) {
let inner = q(0)
let eff = q(1)
let return = q(2)
let tail = q(3)
let input = t.Union(t.RowExtend(label, inner, tail))
let branch = t.Fun(inner, eff, return)
let otherwise = t.Fun(t.Union(tail), eff, return)
let exec = t.Fun(input, eff, return)
pure2(branch, otherwise, exec)
}
pub fn nocases() {
pure1(t.Union(t.Empty), q(0))
}
fn perform(l) {
t.Fun(q(0), t.EffectExtend(l, #(q(0), q(1)), t.Empty), q(1))
}
pub fn handle(label) {
let lift = q(0)
let reply = q(1)
let tail = q(2)
let return = q(3)
let kont = t.Fun(reply, tail, return)
let handler = t.Fun(lift, t.Empty, t.Fun(kont, tail, return))
let exec =
t.Fun(
t.Record(t.Empty),
t.EffectExtend(label, #(lift, reply), tail),
return,
)
t.Fun(handler, t.Empty, t.Fun(exec, tail, return))
}
// equal fn should be open in fn that takes boolean and other union
fn builtin(name) {
list.key_find(builtins(), name)
}
pub fn builtins() {
[
#("equal", pure2(q(0), q(0), t.boolean)),
// debug is an effect because the format is not fully specified
// #("debug", pure1(q(0), t.String)),
// if the passed in constructor raises an effect then fix does too
#("fix", t.Fun(t.Fun(q(0), q(1), q(0)), q(1), q(0))),
// TODO do we want a never type
#("never", pure1(t.Never, q(1))),
// Eval is effectful and so should be an effect, does that mean that Serialize also needs to be an effect
// #(
// "eval",
// t.Fun(q(0), t.EffectExtend("Eval", #(t.unit, t.unit), t.Empty), q(1)),
// ),
// #("serialize", pure1(q(0), t.String)),
// #("capture", pure1(q(0), t.ast())),
// An effect or something that is built in EYG itself
// #("to_javascript", pure2(q(0), q(1), t.String)),
// These should be in EYG or effects if needed
// #("encode_uri", pure1(t.String, t.String)),
// #("decode_uri_component", pure1(t.String, t.String)),
// #("base64_encode", pure1(t.Binary, t.String)),
#("int_compare", {
let return = t.union([#("Lt", t.unit), #("Eq", t.unit), #("Gt", t.unit)])
pure2(t.Integer, t.Integer, return)
}),
#("int_add", pure2(t.Integer, t.Integer, t.Integer)),
#("int_subtract", pure2(t.Integer, t.Integer, t.Integer)),
#("int_multiply", pure2(t.Integer, t.Integer, t.Integer)),
#("int_divide", pure2(t.Integer, t.Integer, t.result(t.Integer, t.unit))),
#("int_absolute", pure1(t.Integer, t.Integer)),
// Removed as negate is subtract(0, x) or multiply(-1, x)
// #("int_negate", pure1(t.Integer, t.Integer)),
#("int_parse", pure1(t.String, t.result(t.Integer, t.unit))),
#("int_to_string", pure1(t.Integer, t.String)),
// string
#("string_append", pure2(t.String, t.String, t.String)),
#("string_split", {
let return = t.record([#("head", t.String), #("tail", t.List(t.String))])
pure2(t.String, t.String, return)
}),
#("string_split_once", {
let return = t.record([#("head", t.String), #("tail", t.String)])
pure2(t.String, t.String, t.result(return, t.unit))
}),
#("string_replace", pure3(t.String, t.String, t.String, t.String)),
#("string_uppercase", pure1(t.String, t.String)),
#("string_lowercase", pure1(t.String, t.String)),
// pop prefix only works for start with. I'm not sure pop prefix is the format we want to stay with
#("string_starts_with", pure2(t.String, t.String, t.boolean)),
#("string_ends_with", pure2(t.String, t.String, t.boolean)),
#("string_length", pure1(t.String, t.Integer)),
// #("pop_grapheme", {
// let return = t.record([#("head", t.String), #("tail", t.String)])
// pure1(t.String, t.result(return, t.unit))
// }),
// #("pop_prefix", {
// let eff = q(0)
// let return = q(1)
// let yes = t.Fun(t.String, eff, return)
// let no = t.Fun(t.unit, eff, return)
// t.Fun(
// t.String,
// t.Empty,
// t.Fun(t.String, t.Empty, t.Fun(yes, t.Empty, t.Fun(no, eff, return))),
// )
// }),
#("string_to_binary", pure1(t.String, t.Binary)),
#("string_from_binary", pure1(t.Binary, t.result(t.String, t.unit))),
// This should be literals
#("binary_from_integers", pure1(t.List(t.Integer), t.Binary)),
#("binary_size", pure1(t.Binary, t.Integer)),
#("binary_concat", pure2(t.Binary, t.Binary, t.Binary)),
#("binary_compare", {
let return = t.union([#("Lt", t.unit), #("Eq", t.unit), #("Gt", t.unit)])
pure2(t.Binary, t.Binary, return)
}),
#("binary_fold", {
let acc = q(1)
// eff only thrown by reduce when last argument given
let eff = q(2)
let reducer = t.Fun(t.Integer, eff, t.Fun(acc, eff, acc))
pure2(t.Binary, acc, t.Fun(reducer, eff, acc))
}),
// Don't optimise for object creation
// #("uncons", {
// let el = q(0)
// let eff = q(1)
// let return = q(2)
// let empty = t.Fun(t.unit, eff, return)
// let nonempty = t.Fun(el, eff, t.Fun(t.List(el), eff, return))
// t.Fun(
// t.List(el),
// t.Empty,
// t.Fun(empty, t.Empty, t.Fun(nonempty, eff, return)),
// )
// }),
#("list_pop", {
let return = t.record([#("head", q(0)), #("tail", t.List(q(0)))])
pure1(t.List(q(0)), t.result(return, t.unit))
}),
#("list_fold", {
let el = q(0)
let acc = q(1)
// eff only thrown by reduce when last argument given
let eff = q(2)
let reducer = t.Fun(el, eff, t.Fun(acc, eff, acc))
pure2(t.List(el), acc, t.Fun(reducer, eff, acc))
}),
]
}