diff --git a/eyg/src/atelier/app.gleam b/eyg/src/atelier/app.gleam index 848471b7b..5a84f5f0a 100644 --- a/eyg/src/atelier/app.gleam +++ b/eyg/src/atelier/app.gleam @@ -3,6 +3,7 @@ import gleam/result import gleam/io import gleam/int import gleam/list +import gleam/map import gleam/option.{None, Option, Some} import gleam/string import gleam/fetch @@ -10,17 +11,34 @@ import gleam/http import gleam/http/request import lustre/cmd import atelier/transform.{Act} -import eygir/expression as e +import eyg/incremental/source as e +import eyg/incremental/cursor import eygir/encode import eyg/analysis/inference import eyg/runtime/standard +import eyg/incremental/store +import plinth/javascript/map as mutable_map +import eyg/analysis/jm/incremental as jm +import eyg/analysis/jm/type_ as jmt +import eyg/analysis/jm/error +import eyg/analysis/jm/tree pub type WorkSpace { WorkSpace( selection: List(Int), - source: e.Expression, - inferred: Option(inference.Infered), + source: e.Source, + root: Int, + // TODO call acc + inferred: #( + map.Map(Int, jmt.Type), + Int, + Option( + map.Map(Int, Result(jmt.Type, #(error.Reason, jmt.Type, jmt.Type))), + ), + ), mode: Mode, + // copy pasting makes the id get reused that will change the type or have the type used more than once in a tree + // Need to not type things with free variables/types yanked: Option(e.Expression), error: Option(String), history: #( @@ -31,11 +49,11 @@ pub type WorkSpace { } pub type Mode { - Navigate(actions: transform.Act) - WriteLabel(value: String, commit: fn(String) -> e.Expression) - WriteText(value: String, commit: fn(String) -> e.Expression) - WriteNumber(value: Int, commit: fn(Int) -> e.Expression) - WriteTerm(value: String, commit: fn(e.Expression) -> e.Expression) + Navigate(cursor: cursor.Cursor) + WriteLabel(value: String, commit: fn(String) -> #(Int, e.Source)) + WriteText(value: String, commit: fn(String) -> #(Int, e.Source)) + WriteNumber(value: Int, commit: fn(Int) -> #(Int, e.Source)) + WriteTerm(value: String, commit: fn(e.Expression) -> #(Int, e.Source)) } pub type Action { @@ -46,14 +64,80 @@ pub type Action { ClickOption(chosen: e.Expression) } -pub fn init(source) { - let assert Ok(act) = transform.prepare(source, []) - let mode = Navigate(act) +external fn pnow() -> Int = + "" "performance.now" + +pub fn init(tree) { + let start = pnow() + // TODO don't use store with mutable ref typing and free is unused + let #(root, s) = store.load(store.empty(), tree) + let source = s.source + io.debug(#( + "loading store took ms:", + pnow() - start, + root, + map.size(s.source), + map.size(s.free), + )) + + let sub = map.new() + let next = 0 + let env = map.new() + // TODO type for editor + let type_ = jmt.Var(-1) + let eff = jmt.Var(-2) + let types = map.new() + + let start = pnow() + let #(sub, next, typesjm) as acc = + jm.infer(sub, next, env, source, root, type_, eff, types) + + io.debug(#("typing jm took ms:", pnow() - start, map.size(typesjm))) + list.map( + map.to_list(typesjm), + fn(r) { + let #(id, r) = r + case r { + Ok(_) -> Nil + Error(reason) -> { + io.debug("================ type error") + io.debug(map.get(s.source, id)) + // io.debug(reason) + io.debug(jmt.resolve_error(reason, sub)) + Nil + } + } + }, + ) + + let _ = { + // TODO type for editor + let type_ = jmt.Var(-1) + let eff = jmt.Var(-2) + + let start = pnow() + let #(sub, next, types) = tree.infer(tree, type_, eff) + io.debug(#("typing jm took ms:", pnow() - start, map.size(types))) + types + |> map.to_list + |> list.filter_map(fn(z) { + let #(p, r) = z + case r { + Ok(_) -> Error(Nil) + Error(reason) -> Ok(io.debug(reason)) + } + }) + } + io.debug("d000---") + // cusor at root doesn't ever error + let assert Ok(c) = cursor.at([], root, source) + let mode = Navigate(c) // Have inference work once for showing elements but need to also background this WorkSpace( [], source, - Some(standard.infer(source)), + root, + #(sub, next, Some(typesjm)), mode, None, None, @@ -99,8 +183,8 @@ pub fn update(state: WorkSpace, action) { } pub fn select_node(state, path) { - let WorkSpace(source: source, ..) = state - let assert Ok(act) = transform.prepare(source, path) + let WorkSpace(source: source, root: root, ..) = state + let assert Ok(act) = cursor.at(path, root, source) let mode = Navigate(act) let state = WorkSpace(..state, source: source, selection: path, mode: mode) @@ -115,35 +199,35 @@ pub fn keypress(key, state: WorkSpace) { let r = case state.mode, key { // save in this state only because q is a normal letter needed when entering text Navigate(_act), "q" -> save(state) - Navigate(act), "w" -> call_with(act, state) + // Navigate(act), "w" -> call_with(act, state) Navigate(act), "e" -> Ok(assign_to(act, state)) - Navigate(act), "r" -> record(act, state) - Navigate(act), "t" -> Ok(tag(act, state)) - Navigate(act), "y" -> Ok(copy(act, state)) - // copy paste quite rare so we use upper case. might be best as command - Navigate(act), "Y" -> paste(act, state) - Navigate(act), "u" -> unwrap(act, state) - Navigate(act), "i" -> insert(act, state) - Navigate(act), "o" -> overwrite(act, state) - Navigate(act), "p" -> Ok(perform(act, state)) + // Navigate(act), "r" -> record(act, state) + // Navigate(act), "t" -> Ok(tag(act, state)) + // Navigate(act), "y" -> Ok(copy(act, state)) + // // copy paste quite rare so we use upper case. might be best as command + // Navigate(act), "Y" -> paste(act, state) + // Navigate(act), "u" -> unwrap(act, state) + // Navigate(act), "i" -> insert(act, state) + // Navigate(act), "o" -> overwrite(act, state) + // Navigate(act), "p" -> Ok(perform(act, state)) Navigate(_act), "a" -> increase(state) Navigate(act), "s" -> decrease(act, state) Navigate(act), "d" -> delete(act, state) - Navigate(act), "f" -> Ok(abstract(act, state)) - Navigate(act), "g" -> select(act, state) - Navigate(act), "h" -> handle(act, state) - // Navigate(act), "j" -> ("down probably not") - // Navigate(act), "k" -> ("up probably not") - // Navigate(act), "l" -> ("right probably not") - Navigate(_act), "z" -> undo(state) - Navigate(_act), "Z" -> redo(state) - Navigate(act), "x" -> list(act, state) - Navigate(act), "c" -> call(act, state) - Navigate(act), "v" -> Ok(variable(act, state)) - Navigate(act), "b" -> Ok(binary(act, state)) - Navigate(act), "n" -> Ok(number(act, state)) - Navigate(act), "m" -> match(act, state) - Navigate(act), "M" -> nocases(act, state) + // Navigate(act), "f" -> Ok(abstract(act, state)) + // Navigate(act), "g" -> select(act, state) + // Navigate(act), "h" -> handle(act, state) + // // Navigate(act), "j" -> ("down probably not") + // // Navigate(act), "k" -> ("up probably not") + // // Navigate(act), "l" -> ("right probably not") + // Navigate(_act), "z" -> undo(state) + // Navigate(_act), "Z" -> redo(state) + // Navigate(act), "x" -> list(act, state) + // Navigate(act), "c" -> call(act, state) + // Navigate(act), "v" -> Ok(variable(act, state)) + // Navigate(act), "b" -> Ok(binary(act, state)) + // Navigate(act), "n" -> Ok(number(act, state)) + // Navigate(act), "m" -> match(act, state) + // Navigate(act), "M" -> nocases(act, state) // Navigate(act), " " -> ("space follow suggestion next error") Navigate(_), _ -> Error("no action for keypress") // Other mode @@ -160,12 +244,17 @@ pub fn keypress(key, state: WorkSpace) { WriteText(_, _), _k -> Ok(state) WriteTerm(new, commit), k if k == "Enter" -> { let assert [var, ..selects] = string.split(new, ".") + case selects { + [] -> Nil + _ -> todo("handle dot vars") + } let expression = - list.fold( - selects, - e.Variable(var), - fn(acc, select) { e.Apply(e.Select(select), acc) }, - ) + // list.fold( + // selects, + // e.Variable(var), + // fn(acc, select) { e.Apply(e.Select(select), acc) }, + // ) + e.Var(var) let source = commit(expression) update_source(state, source) } @@ -189,328 +278,363 @@ fn save(state: WorkSpace) { |> request.set_host("localhost:5000") |> request.set_path("/save") |> request.prepend_header("content-type", "application/json") - |> request.set_body(encode.to_json(state.source)) + todo("finish saving") + // |> request.set_body(encode.to_json(state.source)) fetch.send(request) |> io.debug Ok(state) } -fn call_with(act: Act, state) { - let source = act.update(e.Apply(e.Vacant(""), act.target)) - update_source(state, source) -} +// fn call_with(act: Act, state) { +// let source = act.update(e.Apply(e.Vacant(""), act.target)) +// update_source(state, source) +// } // e is essentially line above on a let statement. // nested lets can only be created from the value on the right. // moving something to a module might just have to be copy paste -fn assign_to(act: Act, state) { - let commit = case act.target { +fn assign_to(c, state: WorkSpace) { + let assert Ok(exp) = cursor.expression(c, state.source) + let #(hole, source) = e.insert(state.source, e.Vacant("")) + let commit = case exp { e.Let(_, _, _) -> fn(text) { - act.update(e.Let(text, e.Vacant(""), act.target)) + // TODO needs a tree replace + // easier to insert tree but only if no reference to the old stuff + let assert Ok(r) = + store.replace(source, c, e.Let(text, hole, cursor.inner(c))) + r } // normally I want to add something above - exp -> fn(text) { act.update(e.Let(text, e.Vacant(""), exp)) } - } - WorkSpace(..state, mode: WriteLabel("", commit)) -} - -fn record(act: Act, state) { - case act.target { - e.Vacant(_comment) -> - act.update(e.Empty) - |> update_source(state, _) - e.Empty as exp | e.Apply(e.Apply(e.Extend(_), _), _) as exp -> { - let commit = fn(text) { - act.update(e.Apply(e.Apply(e.Extend(text), e.Vacant("")), exp)) - } - Ok(WorkSpace(..state, mode: WriteLabel("", commit))) - } - exp -> { - let commit = fn(text) { - act.update(e.Apply(e.Apply(e.Extend(text), exp), e.Empty)) - } - Ok(WorkSpace(..state, mode: WriteLabel("", commit))) + exp -> fn(text) { + // always the same + // act.update(e.Let(text, e.Vacant(""), exp)) } + let assert Ok(r) = + store.replace(source, c, e.Let(text, hole, cursor.inner(c))) + r } } -} - -fn tag(act: Act, state) { - let commit = case act.target { - e.Vacant(_comment) -> fn(text) { act.update(e.Tag(text)) } - exp -> fn(text) { act.update(e.Apply(e.Tag(text), exp)) } - } WorkSpace(..state, mode: WriteLabel("", commit)) } -fn copy(act: Act, state) { - WorkSpace(..state, yanked: Some(act.target)) -} - -fn paste(act: Act, state: WorkSpace) { - case state.yanked { - Some(snippet) -> { - let source = act.update(snippet) - update_source(state, source) - } - None -> Error("nothing on clipboard") - } -} - -fn unwrap(act: Act, state) { - case act.parent { - None -> Error("top level") - Some(#(_i, _list, _, parent_update)) -> { - let source = parent_update(act.target) - update_source(state, source) - } - } -} - -fn insert(act: Act, state) { - let write = fn(text, build) { - WriteLabel(text, fn(new) { act.update(build(new)) }) - } - use mode <- result.then(case act.target { - e.Variable(value) -> Ok(write(value, e.Variable(_))) - e.Lambda(param, body) -> Ok(write(param, e.Lambda(_, body))) - e.Apply(_, _) -> Error("no insert option for apply") - e.Let(var, body, then) -> Ok(write(var, e.Let(_, body, then))) - - e.Binary(value) -> - Ok(WriteText(value, fn(new) { act.update(e.Binary(new)) })) - e.Integer(value) -> - Ok(WriteNumber(value, fn(new) { act.update(e.Integer(new)) })) - e.Tail | e.Cons -> Error("there is no insert for lists") - e.Vacant(comment) -> Ok(write(comment, e.Vacant)) - e.Empty -> Error("empty record no insert") - e.Extend(label) -> Ok(write(label, e.Extend)) - e.Select(label) -> Ok(write(label, e.Select)) - e.Overwrite(label) -> Ok(write(label, e.Overwrite)) - e.Tag(label) -> Ok(write(label, e.Tag)) - e.Case(label) -> Ok(write(label, e.Case)) - e.NoCases -> Error("no cases") - e.Perform(label) -> Ok(write(label, e.Perform)) - e.Handle(label) -> Ok(write(label, e.Handle)) - e.Builtin(_) -> Error("no insert option for builtin, use stdlib references") - }) - - Ok(WorkSpace(..state, mode: mode)) -} - -fn overwrite(act: Act, state) { - case act.target { - e.Apply(e.Apply(e.Overwrite(_), _), _) as exp -> { - let commit = fn(text) { - act.update(e.Apply(e.Apply(e.Overwrite(text), e.Vacant("")), exp)) - } - Ok(WorkSpace(..state, mode: WriteLabel("", commit))) - } - exp -> { - let commit = fn(text) { - // This is the same as above - act.update(e.Apply(e.Apply(e.Overwrite(text), e.Vacant("")), exp)) - } - Ok(WorkSpace(..state, mode: WriteLabel("", commit))) - } - } -} +// fn record(act: Act, state) { +// case act.target { +// e.Vacant(_comment) -> +// act.update(e.Empty) +// |> update_source(state, _) +// e.Empty as exp | e.Apply(e.Apply(e.Extend(_), _), _) as exp -> { +// let commit = fn(text) { +// act.update(e.Apply(e.Apply(e.Extend(text), e.Vacant("")), exp)) +// } +// Ok(WorkSpace(..state, mode: WriteLabel("", commit))) +// } +// exp -> { +// let commit = fn(text) { +// act.update(e.Apply(e.Apply(e.Extend(text), exp), e.Empty)) +// } +// Ok(WorkSpace(..state, mode: WriteLabel("", commit))) +// } +// } +// } + +// fn tag(act: Act, state) { +// let commit = case act.target { +// e.Vacant(_comment) -> fn(text) { act.update(e.Tag(text)) } +// exp -> fn(text) { act.update(e.Apply(e.Tag(text), exp)) } +// } +// WorkSpace(..state, mode: WriteLabel("", commit)) +// } + +// fn copy(act: Act, state) { +// WorkSpace(..state, yanked: Some(act.target)) +// } + +// fn paste(act: Act, state: WorkSpace) { +// case state.yanked { +// Some(snippet) -> { +// let source = act.update(snippet) +// update_source(state, source) +// } +// None -> Error("nothing on clipboard") +// } +// } + +// fn unwrap(act: Act, state) { +// case act.parent { +// None -> Error("top level") +// Some(#(_i, _list, _, parent_update)) -> { +// let source = parent_update(act.target) +// update_source(state, source) +// } +// } +// } + +// fn insert(act: Act, state) { +// let write = fn(text, build) { +// WriteLabel(text, fn(new) { act.update(build(new)) }) +// } +// use mode <- result.then(case act.target { +// e.Variable(value) -> Ok(write(value, e.Variable(_))) +// e.Lambda(param, body) -> Ok(write(param, e.Lambda(_, body))) +// e.Apply(_, _) -> Error("no insert option for apply") +// e.Let(var, body, then) -> Ok(write(var, e.Let(_, body, then))) + +// e.Binary(value) -> +// Ok(WriteText(value, fn(new) { act.update(e.Binary(new)) })) +// e.Integer(value) -> +// Ok(WriteNumber(value, fn(new) { act.update(e.Integer(new)) })) +// e.Tail | e.Cons -> Error("there is no insert for lists") +// e.Vacant(comment) -> Ok(write(comment, e.Vacant)) +// e.Empty -> Error("empty record no insert") +// e.Extend(label) -> Ok(write(label, e.Extend)) +// e.Select(label) -> Ok(write(label, e.Select)) +// e.Overwrite(label) -> Ok(write(label, e.Overwrite)) +// e.Tag(label) -> Ok(write(label, e.Tag)) +// e.Case(label) -> Ok(write(label, e.Case)) +// e.NoCases -> Error("no cases") +// e.Perform(label) -> Ok(write(label, e.Perform)) +// e.Handle(label) -> Ok(write(label, e.Handle)) +// e.Builtin(_) -> Error("no insert option for builtin, use stdlib references") +// }) + +// Ok(WorkSpace(..state, mode: mode)) +// } + +// fn overwrite(act: Act, state) { +// case act.target { +// e.Apply(e.Apply(e.Overwrite(_), _), _) as exp -> { +// let commit = fn(text) { +// act.update(e.Apply(e.Apply(e.Overwrite(text), e.Vacant("")), exp)) +// } +// Ok(WorkSpace(..state, mode: WriteLabel("", commit))) +// } +// exp -> { +// let commit = fn(text) { +// // This is the same as above +// act.update(e.Apply(e.Apply(e.Overwrite(text), e.Vacant("")), exp)) +// } +// Ok(WorkSpace(..state, mode: WriteLabel("", commit))) +// } +// } +// } fn increase(state: WorkSpace) { use selection <- result.then(case list.reverse(state.selection) { [_, ..rest] -> Ok(list.reverse(rest)) [] -> Error("no increase") }) - let assert Ok(act) = transform.prepare(state.source, selection) - Ok(WorkSpace(..state, selection: selection, mode: Navigate(act))) + let assert Ok(c) = cursor.at(selection, state.root, state.source) + Ok(WorkSpace(..state, selection: selection, mode: Navigate(c))) } fn decrease(_act, state: WorkSpace) { let selection = list.append(state.selection, [0]) - use act <- result.then(transform.prepare(state.source, selection)) - Ok(WorkSpace(..state, selection: selection, mode: Navigate(act))) + use c <- result.then( + cursor.at(selection, state.root, state.source) + |> result.map_error(fn(_: Nil) { "no valid decrease" }), + ) + Ok(WorkSpace(..state, selection: selection, mode: Navigate(c))) } -fn delete(act: Act, state) { +fn delete(c, state: WorkSpace) { // an assignment vacant or not is always deleted. // when deleting with a vacant as a target there is no change // we can instead bump up the path - let source = case act.target { - e.Let(_label, _, then) -> act.update(then) - _ -> act.update(e.Vacant("")) - } - update_source(state, source) -} - -fn abstract(act: Act, state) { - let commit = case act.target { - e.Let(label, value, then) -> fn(text) { - act.update(e.Let(label, e.Lambda(text, value), then)) - } - exp -> fn(text) { act.update(e.Lambda(text, exp)) } - } - WorkSpace(..state, mode: WriteLabel("", commit)) -} - -fn select(act: Act, state) { - case act.target { - e.Let(_label, _value, _then) -> Error("can't get on let") - exp -> { - let commit = fn(text) { act.update(e.Apply(e.Select(text), exp)) } - Ok(WorkSpace(..state, mode: WriteLabel("", commit))) - } - } -} - -fn handle(act: Act, state) { - case act.target { - e.Let(_label, _value, _then) -> Error("can't handle on let") - exp -> { - let commit = fn(text) { - act.update(e.Apply(e.Apply(e.Handle(text), e.Vacant("")), exp)) - } - Ok(WorkSpace(..state, mode: WriteLabel("", commit))) - } - } -} - -fn perform(act: Act, state) { - let commit = case act.target { - e.Let(label, _value, then) -> fn(text) { - act.update(e.Let(label, e.Perform(text), then)) - } - _exp -> fn(text) { act.update(e.Perform(text)) } - } - WorkSpace(..state, mode: WriteLabel("", commit)) -} - -fn undo(state: WorkSpace) { - case state.history { - #([], _) -> Error("No history") - #([#(source, selection), ..rest], forward) -> { - let history = #(rest, [#(state.source, state.selection), ..forward]) - use act <- result.then(transform.prepare(source, selection)) - // Has to already be in navigate mode to undo - let mode = Navigate(act) - Ok( - WorkSpace( - ..state, - source: source, - selection: selection, - mode: mode, - history: history, - ), - ) - } - } -} - -fn redo(state: WorkSpace) { - case state.history { - #(_, []) -> Error("No redo") - #(backward, [#(source, selection), ..rest]) -> { - let history = #([#(state.source, state.selection), ..backward], rest) - use act <- result.then(transform.prepare(source, selection)) - // Has to already be in navigate mode to undo - let mode = Navigate(act) - Ok( - WorkSpace( - ..state, - source: source, - selection: selection, - mode: mode, - history: history, - ), - ) - } - } -} - -fn list(act: Act, state) { - let new = case act.target { - e.Vacant(_comment) -> e.Tail - e.Tail | e.Apply(e.Apply(e.Cons, _), _) -> - e.Apply(e.Apply(e.Cons, e.Vacant("")), act.target) - _ -> e.Apply(e.Apply(e.Cons, act.target), e.Tail) - } - let source = act.update(new) - update_source(state, source) -} - -fn call(act: Act, state) { - let source = act.update(e.Apply(act.target, e.Vacant(""))) - update_source(state, source) -} - -fn variable(act: Act, state) { - let commit = case act.target { - e.Let(label, _value, then) -> fn(term) { - act.update(e.Let(label, term, then)) - } - _exp -> fn(term) { act.update(term) } + let start = pnow() + let assert Ok(exp) = cursor.expression(c, state.source) + let #(hole, source) = e.insert(state.source, e.Vacant("")) + let assert Ok(source) = case exp { + e.Let(_label, _, then) -> e.replace(source, c, then) + _ -> e.replace(source, c, hole) } - WorkSpace(..state, mode: WriteTerm("", commit)) -} - -fn binary(act: Act, state) { - let commit = case act.target { - e.Let(label, _value, then) -> fn(text) { - act.update(e.Let(label, e.Binary(text), then)) - } - _exp -> fn(text) { act.update(e.Binary(text)) } - } - WorkSpace(..state, mode: WriteText("", commit)) -} - -fn number(act: Act, state) { - let #(v, commit) = case act.target { - e.Let(label, _value, then) -> #( - 0, - fn(value) { act.update(e.Let(label, e.Integer(value), then)) }, - ) - e.Integer(value) -> #(value, fn(value) { act.update(e.Integer(value)) }) - _exp -> #(0, fn(value) { act.update(e.Integer(value)) }) - } - WorkSpace(..state, mode: WriteNumber(v, commit)) -} - -fn match(act: Act, state) { - let commit = case act.target { - // e.Let(label, value, then) -> fn(text) { - // act.update(e.Let(label, e.Binary(text), then)) - // } - // Match on original value should maybe be the arg? but I like promoting first class everything - exp -> fn(text) { - act.update(e.Apply(e.Apply(e.Case(text), e.Vacant("")), exp)) - } - } - Ok(WorkSpace(..state, mode: WriteLabel("", commit))) -} - -fn nocases(act: Act, state) { - update_source(state, act.update(e.NoCases)) -} + let ret = update_source(state, source) + io.debug(#("normal update took ms:", pnow() - start)) + + ret +} + +// fn abstract(act: Act, state) { +// let commit = case act.target { +// e.Let(label, value, then) -> fn(text) { +// act.update(e.Let(label, e.Lambda(text, value), then)) +// } +// exp -> fn(text) { act.update(e.Lambda(text, exp)) } +// } +// WorkSpace(..state, mode: WriteLabel("", commit)) +// } + +// fn select(act: Act, state) { +// case act.target { +// e.Let(_label, _value, _then) -> Error("can't get on let") +// exp -> { +// let commit = fn(text) { act.update(e.Apply(e.Select(text), exp)) } +// Ok(WorkSpace(..state, mode: WriteLabel("", commit))) +// } +// } +// } + +// fn handle(act: Act, state) { +// case act.target { +// e.Let(_label, _value, _then) -> Error("can't handle on let") +// exp -> { +// let commit = fn(text) { +// act.update(e.Apply(e.Apply(e.Handle(text), e.Vacant("")), exp)) +// } +// Ok(WorkSpace(..state, mode: WriteLabel("", commit))) +// } +// } +// } + +// fn perform(act: Act, state) { +// let commit = case act.target { +// e.Let(label, _value, then) -> fn(text) { +// act.update(e.Let(label, e.Perform(text), then)) +// } +// _exp -> fn(text) { act.update(e.Perform(text)) } +// } +// WorkSpace(..state, mode: WriteLabel("", commit)) +// } + +// fn undo(state: WorkSpace) { +// case state.history { +// #([], _) -> Error("No history") +// #([#(source, selection), ..rest], forward) -> { +// let history = #(rest, [#(state.source, state.selection), ..forward]) +// use act <- result.then(transform.prepare(source, selection)) +// // Has to already be in navigate mode to undo +// let mode = Navigate(act) +// Ok( +// WorkSpace( +// ..state, +// source: source, +// selection: selection, +// mode: mode, +// history: history, +// ), +// ) +// } +// } +// } + +// fn redo(state: WorkSpace) { +// case state.history { +// #(_, []) -> Error("No redo") +// #(backward, [#(source, selection), ..rest]) -> { +// let history = #([#(state.source, state.selection), ..backward], rest) +// use act <- result.then(transform.prepare(source, selection)) +// // Has to already be in navigate mode to undo +// let mode = Navigate(act) +// Ok( +// WorkSpace( +// ..state, +// source: source, +// selection: selection, +// mode: mode, +// history: history, +// ), +// ) +// } +// } +// } + +// fn list(act: Act, state) { +// let new = case act.target { +// e.Vacant(_comment) -> e.Tail +// e.Tail | e.Apply(e.Apply(e.Cons, _), _) -> +// e.Apply(e.Apply(e.Cons, e.Vacant("")), act.target) +// _ -> e.Apply(e.Apply(e.Cons, act.target), e.Tail) +// } +// let source = act.update(new) +// update_source(state, source) +// } + +// fn call(act: Act, state) { +// let source = act.update(e.Apply(act.target, e.Vacant(""))) +// update_source(state, source) +// } + +// fn variable(act: Act, state) { +// let commit = case act.target { +// e.Let(label, _value, then) -> fn(term) { +// act.update(e.Let(label, term, then)) +// } +// _exp -> fn(term) { act.update(term) } +// } +// WorkSpace(..state, mode: WriteTerm("", commit)) +// } + +// fn binary(act: Act, state) { +// let commit = case act.target { +// e.Let(label, _value, then) -> fn(text) { +// act.update(e.Let(label, e.Binary(text), then)) +// } +// _exp -> fn(text) { act.update(e.Binary(text)) } +// } +// WorkSpace(..state, mode: WriteText("", commit)) +// } + +// fn number(act: Act, state) { +// let #(v, commit) = case act.target { +// e.Let(label, _value, then) -> #( +// 0, +// fn(value) { act.update(e.Let(label, e.Integer(value), then)) }, +// ) +// e.Integer(value) -> #(value, fn(value) { act.update(e.Integer(value)) }) +// _exp -> #(0, fn(value) { act.update(e.Integer(value)) }) +// } +// WorkSpace(..state, mode: WriteNumber(v, commit)) +// } + +// fn match(act: Act, state) { +// let commit = case act.target { +// // e.Let(label, value, then) -> fn(text) { +// // act.update(e.Let(label, e.Binary(text), then)) +// // } +// // Match on original value should maybe be the arg? but I like promoting first class everything +// exp -> fn(text) { +// act.update(e.Apply(e.Apply(e.Case(text), e.Vacant("")), exp)) +// } +// } +// Ok(WorkSpace(..state, mode: WriteLabel("", commit))) +// } + +// fn nocases(act: Act, state) { +// update_source(state, act.update(e.NoCases)) +// } // app state actions maybe separate from ui but maybe ui files organised by mode // update source also ends the entry state -fn update_source(state: WorkSpace, source) { - use act <- result.then(transform.prepare(source, state.selection)) - let mode = Navigate(act) - let #(history, inferred) = case source == state.source { - True -> #(state.history, state.inferred) - False -> { - let #(backwards, _forwards) = state.history - let history = #([#(state.source, state.selection), ..backwards], []) - #(history, None) - } - } - Ok( - WorkSpace( - ..state, - source: source, - mode: mode, - history: history, - inferred: inferred, - ), +fn update_source(state: WorkSpace, next) { + let #(root, source) = next + use cursor <- result.then( + cursor.at(state.selection, root, source) + |> result.map_error(fn(_: Nil) { "nope on the update" }), ) + + let mode = Navigate(cursor) + // let #(history, inferred) = case source == state.source { + // True -> #(state.history, state.inferred) + // False -> { + // let #(backwards, _forwards) = state.history + // let history = #([#(state.source, state.selection), ..backwards], []) + // #(history, None) + // } + // } + + let sub = map.new() + let next = 0 + let env = map.new() + // TODO type for editor + let type_ = jmt.Var(-1) + let eff = jmt.Var(-2) + let types = map.new() + + let start = pnow() + let #(sub, _next, typesjm) = + jm.infer(sub, next, env, source, root, type_, eff, types) + io.debug(#("typing jm took ms:", pnow() - start, map.size(typesjm))) + + Ok(WorkSpace(..state, source: source, root: root, mode: mode)) + // history: history, + // inferred: inferred, } diff --git a/eyg/src/atelier/view/pallet.gleam b/eyg/src/atelier/view/pallet.gleam index f706f5cd7..f0ba601b1 100644 --- a/eyg/src/atelier/view/pallet.gleam +++ b/eyg/src/atelier/view/pallet.gleam @@ -1,4 +1,5 @@ // command pallet +import gleam/io import gleam/dynamic import gleam/list import gleam/map @@ -8,19 +9,22 @@ import lustre/element.{div, input, span, text, textarea} import lustre/event.{on_click} import lustre/attribute.{class, classes} import eyg/analysis/inference +import eyg/incremental/cursor +import eyg/analysis/jm/error +import eyg/analysis/jm/type_ as t import atelier/app.{ClickOption, SelectNode} -import atelier/view/typ +import atelier/view/type_ import atelier/inventory -pub fn render(state: app.WorkSpace, inferred) { +pub fn render(state: app.WorkSpace) { div( [class("cover bg-gray-100")], case state.mode { app.WriteLabel(value, _) -> render_label(value) - app.WriteTerm(value, _) -> render_variable(value, inferred, state) + app.WriteTerm(value, _) -> render_variable(value, state) app.WriteNumber(number, _) -> render_number(number) app.WriteText(value, _) -> render_text(value) - _ -> render_navigate(inferred, state) + _ -> render_navigate(state) }, ) } @@ -36,39 +40,35 @@ fn render_label(value) { ] } -fn render_variable( - value, - inferred: option.Option(inference.Infered), - state: app.WorkSpace, -) { +fn render_variable(value, state: app.WorkSpace) { [ - div( - [], - case inferred { - Some(inferred) -> - case inventory.variables_at(inferred.environments, state.selection) { - // using spaces because we are in pre tag and text based - // not in pre tag here - Ok(options) -> - list.map( - options, - fn(option) { - let #(t, term) = option - span( - [ - class("rounded bg-blue-100 p-1"), - on_click(ClickOption(term)), - ], - [text(t)], - ) - }, - ) - |> list.intersperse(text(" ")) - Error(_) -> [text("no env")] - } - None -> [] - }, - ), + div([], []), + // case inferred { + // Some(inferred) -> + // // TODO use inferred but we dont have the whole env map. + // [] + // // case inventory.variables_at(inferred.environments, state.selection) { + // // // using spaces because we are in pre tag and text based + // // // not in pre tag here + // // Ok(options) -> + // // list.map( + // // options, + // // fn(option) { + // // let #(t, term) = option + // // span( + // // [ + // // class("rounded bg-blue-100 p-1"), + // // on_click(ClickOption(term)), + // // ], + // // [text(t)], + // // ) + // // }, + // // ) + // // |> list.intersperse(text(" ")) + // // Error(_) -> [text("no env")] + // // } + // None -> [] + // }, input([ class("border w-full"), attribute.autofocus(True), @@ -114,10 +114,11 @@ fn render_text(value) { ] } -fn render_navigate(inferred, state: app.WorkSpace) { +fn render_navigate(state: app.WorkSpace) { + let #(sub, _next, types) = state.inferred [ - case inferred { - Some(inferred) -> render_errors(inferred) + case types { + Some(types) -> render_errors(types) None -> span([], []) }, div( @@ -126,13 +127,25 @@ fn render_navigate(inferred, state: app.WorkSpace) { span( [], [ - case inferred { - Some(inferred) -> + case types { + Some(types) -> text(string.append( ":", - inferred - |> inference.type_of(state.selection) - |> typ.render(), + { + let assert Ok(c) = + cursor.at(state.selection, state.root, state.source) + let id = cursor.inner(c) + let assert Ok(type_) = map.get(types, id) + case type_ { + Ok(type_) -> type_.render_type(t.resolve(type_, sub)) + Error(#(reason, t1, t2)) -> + type_.render_failure( + reason, + t.resolve(t1, sub), + t.resolve(t2, sub), + ) + } + }, )) None -> span([], [text("type checking")]) }, @@ -151,10 +164,10 @@ fn render_navigate(inferred, state: app.WorkSpace) { ] } -fn render_errors(inferred: inference.Infered) { +fn render_errors(types) { let errors = list.filter_map( - map.to_list(inferred.types), + map.to_list(types), fn(el) { let #(k, v) = el case v { @@ -173,12 +186,22 @@ fn render_errors(inferred: inference.Infered) { errors, fn(err) { let #(path, reason) = err + // TODO real path + let path = [] div( [classes([#("cursor-pointer", True)]), on_click(SelectNode(path))], - [text(typ.render_failure(reason))], + [text(render_failure(reason))], ) }, ), ) } } + +pub fn render_failure(reason) { + let #(reason, _, _) = reason + case reason { + error.RecursiveType -> "recursive type" + _ -> "other error" + } +} diff --git a/eyg/src/atelier/view/root.gleam b/eyg/src/atelier/view/root.gleam index 36e635d1e..fe49f6511 100644 --- a/eyg/src/atelier/view/root.gleam +++ b/eyg/src/atelier/view/root.gleam @@ -1,13 +1,16 @@ import gleam/io +import gleam/option import lustre/element.{div} import lustre/attribute.{attribute, autofocus, class} import lustre/event.{on_keydown} +import eyg/incremental/source import atelier/app import atelier/view/projection import atelier/view/pallet // maybe belongs in procejection .render pub fn render(state: app.WorkSpace) { + let assert Ok(tree) = source.to_tree(state.source, state.root) div( [ on_keydown(app.Keypress), @@ -19,9 +22,10 @@ pub fn render(state: app.WorkSpace) { ], [ div([class("expand")], []), - projection.render(state.source, state.selection, state.inferred), + // pass through inferred + projection.render(tree, state.selection, option.None), div([class("expand")], []), - pallet.render(state, state.inferred), + pallet.render(state), ], ) } diff --git a/eyg/src/atelier/view/typ.gleam b/eyg/src/atelier/view/typ.gleam index 61a039a99..988cda837 100644 --- a/eyg/src/atelier/view/typ.gleam +++ b/eyg/src/atelier/view/typ.gleam @@ -6,6 +6,7 @@ import eyg/analysis/typ as t import eyg/analysis/unification import eyg/analysis/substitutions as sub +// Keep for old kinded version // Do we have a general need for type debug functionality pub fn render(type_info) { case type_info { diff --git a/eyg/src/atelier/view/type_.gleam b/eyg/src/atelier/view/type_.gleam new file mode 100644 index 000000000..45239191e --- /dev/null +++ b/eyg/src/atelier/view/type_.gleam @@ -0,0 +1,95 @@ +// for flat types +import gleam/int +import gleam/list +import gleam/string +import eyg/analysis/jm/type_ as t +import eyg/analysis/jm/error + +pub fn render_failure(reason, t1, t2) { + case reason { + error.TypeMismatch(a, b) -> + // need to shrink errors together + string.concat(["Type Missmatch: ", render_type(a), " vs ", render_type(b)]) + error.RowMismatch(label) -> string.append("Row Missmatch: ", label) + error.MissingVariable(x) -> string.append("missing variable: ", x) + error.RecursiveType -> "Recursive type" + error.InvalidTail(_) -> "invalid tail" + } +} + +pub fn render_type(typ) { + case typ { + t.Var(i) -> int.to_string(i) + t.Integer -> "Integer" + t.String -> "String" + t.LinkedList(el) -> string.concat(["List(", render_type(el), ")"]) + t.Fun(from, effects, to) -> + string.concat([ + "(", + render_type(from), + ") ->", + render_effects(effects), + " ", + render_type(to), + ]) + t.Union(row) -> + string.concat([ + "[", + string.concat( + render_row(row) + |> list.intersperse(" | "), + ), + "]", + ]) + t.Record(row) -> + string.concat([ + "{", + string.concat( + render_row(row) + |> list.intersperse(", "), + ), + "}", + ]) + _ -> "invalid should not be raw type" + } +} + +fn render_row(r) -> List(String) { + case r { + t.Empty -> [] + t.Var(i) -> [string.append("..", int.to_string(i))] + t.RowExtend(label, value, tail) -> { + let field = string.concat([label, ": ", render_type(value)]) + [field, ..render_row(tail)] + } + _ -> ["not a valid row"] + } +} + +fn render_effects(effects) { + case effects { + t.Var(_) | t.Empty -> "" + t.EffectExtend(label, #(lift, resume), tail) -> + string.concat([ + " <", + string.join( + collect_effect(tail, [render_effect(label, lift, resume)]), + ", ", + ), + ">", + ]) + _ -> "not a valid effect" + } +} + +fn render_effect(label, lift, resume) { + string.concat([label, "(", render_type(lift), ", ", render_type(resume), ")"]) +} + +fn collect_effect(eff, acc) { + case eff { + t.EffectExtend(label, #(lift, resume), tail) -> + collect_effect(tail, [render_effect(label, lift, resume), ..acc]) + _ -> acc + } +} diff --git a/eyg/src/eyg/analysis/jm/incremental.gleam b/eyg/src/eyg/analysis/jm/incremental.gleam new file mode 100644 index 000000000..6ca5ff065 --- /dev/null +++ b/eyg/src/eyg/analysis/jm/incremental.gleam @@ -0,0 +1,133 @@ +import gleam/io +import gleam/map +import eyg/incremental/source as e +import eyg/analysis/jm/error +import eyg/analysis/jm/type_ as t +import eyg/analysis/jm/infer.{ + Cont, Done, builtins, extend, fetch, generalise, instantiate, loop, mono, + unify_at, +} + +pub fn infer(sub, next, env, source, ref, type_, eff, types) { + loop(step(sub, next, env, source, ref, type_, eff, types, Done)) +} + +// TODO occurs in and recursive row check + +// TODO acc = sub, nex, types +// step(acc, env, source, ref, t0, e0, k) +fn step(sub, next, env, source, ref, type_, eff, types, k) { + case map.get(source, ref) { + Error(Nil) -> Cont(#(sub, next, types), k) + Ok(exp) -> + case exp { + e.Var(x) -> fetch(env, x, sub, next, types, ref, type_, k) + e.Call(e1, e2) -> { + // can't error + let types = map.insert(types, ref, Ok(type_)) + let #(arg, next) = t.fresh(next) + use #(sub, next, types) <- step( + sub, + next, + env, + source, + e1, + t.Fun(arg, eff, type_), + eff, + types, + ) + use #(sub, next, types) <- step( + sub, + next, + env, + source, + e2, + arg, + eff, + types, + ) + Cont(#(sub, next, types), k) + } + e.Fn(x, e1) -> { + let #(arg, next) = t.fresh(next) + let #(new_eff, next) = t.fresh(next) + let #(ret, next) = t.fresh(next) + let #(sub, next, types) = + unify_at(type_, t.Fun(arg, new_eff, ret), sub, next, types, ref) + let env = extend(env, x, mono(arg)) + use #(sub, next, types) <- step( + sub, + next, + env, + source, + e1, + ret, + new_eff, + types, + ) + Cont(#(sub, next, types), k) + } + e.Let(x, e1, e2) -> { + // can't error + let types = map.insert(types, ref, Ok(type_)) + let #(inner, next) = t.fresh(next) + use #(sub, next, types) <- step( + sub, + next, + env, + source, + e1, + inner, + eff, + types, + ) + let env = extend(env, x, generalise(sub, env, inner)) + use #(sub, next, types) <- step( + sub, + next, + env, + source, + e2, + type_, + eff, + types, + ) + Cont(#(sub, next, types), k) + } + e.Builtin(identifier) -> + fetch(builtins(), identifier, sub, next, types, ref, type_, k) + literal -> { + let #(found, next) = primitive(literal, next) + Cont(unify_at(type_, found, sub, next, types, ref), k) + } + } + } +} + +fn primitive(exp, next) { + case exp { + e.Var(_) | e.Call(_, _) | e.Fn(_, _) | e.Let(_, _, _) | e.Builtin(_) -> + panic("not a literal") + e.String(_) -> #(t.String, next) + e.Integer(_) -> #(t.Integer, next) + + e.Tail -> t.tail(next) + e.Cons -> t.cons(next) + e.Vacant(_comment) -> t.fresh(next) + + // Record + e.Empty -> t.empty(next) + e.Extend(label) -> t.extend(label, next) + e.Overwrite(label) -> t.overwrite(label, next) + e.Select(label) -> t.select(label, next) + + // Union + e.Tag(label) -> t.tag(label, next) + e.Case(label) -> t.case_(label, next) + e.NoCases -> t.nocases(next) + + // Effect + e.Perform(label) -> t.perform(label, next) + e.Handle(label) -> t.handle(label, next) + } +} diff --git a/eyg/src/eyg/incremental/cursor.gleam b/eyg/src/eyg/incremental/cursor.gleam new file mode 100644 index 000000000..15274a472 --- /dev/null +++ b/eyg/src/eyg/incremental/cursor.gleam @@ -0,0 +1,51 @@ +import gleam/map +import gleam/result +import eyg/incremental/source as e + +pub type Cursor = + #(List(Int), Int) + +fn zip_match(node, path_element) { + case node, path_element { + e.Let(_, index, _), 0 -> Ok(index) + e.Let(_, _, index), 1 -> Ok(index) + e.Fn(_, index), 0 -> Ok(index) + e.Call(index, _), 0 -> Ok(index) + e.Call(_, index), 1 -> Ok(index) + _, _ -> Error(Nil) + } +} + +fn do_at(path, refs, current, zoom, root) { + case path { + [] -> Ok(#([current, ..zoom], root)) + [path_element, ..path] -> { + use node <- result.then(map.get(refs, current)) + let zoom = [current, ..zoom] + use current <- result.then(zip_match(node, path_element)) + do_at(path, refs, current, zoom, root) + } + } +} + +pub fn at(path, root, refs) { + case path { + [] -> Ok(#([], root)) + [path_element, ..path] -> { + use node <- result.then(map.get(refs, root)) + use current <- result.then(zip_match(node, path_element)) + do_at(path, refs, current, [], root) + } + } +} + +pub fn inner(c) { + case c { + #([], root) -> root + #([ref, ..], _) -> ref + } +} + +pub fn expression(c, source) { + map.get(source, inner(c)) +} diff --git a/eyg/src/eyg/incremental/inference.gleam b/eyg/src/eyg/incremental/inference.gleam new file mode 100644 index 000000000..76179e7b4 --- /dev/null +++ b/eyg/src/eyg/incremental/inference.gleam @@ -0,0 +1,173 @@ +import gleam/io +import gleam/list +import gleam/map +import gleam/option +import gleam/result +import gleam/set +import gleam/setx +import eyg/analysis/typ as t +import eyg/analysis/substitutions as sub +import eyg/analysis/scheme.{Scheme} +import eyg/analysis/unification +import eyg/incremental/source + +fn from_end(items, i) { + list.at(items, list.length(items) - 1 - i) +} + +fn do_free(rest, acc) { + case rest { + [] -> list.reverse(acc) + [node, ..rest] -> { + let free = case node { + source.Var(x) -> setx.singleton(x) + source.Fn(x, ref) -> { + let assert Ok(body) = from_end(acc, ref) + set.delete(body, x) + } + source.Let(x, ref_v, ref_t) -> { + let assert Ok(value) = from_end(acc, ref_v) + let assert Ok(then) = from_end(acc, ref_t) + set.union(value, set.delete(then, x)) + } + source.Call(ref_func, ref_arg) -> { + let assert Ok(func) = from_end(acc, ref_func) + let assert Ok(arg) = from_end(acc, ref_arg) + set.union(func, arg) + } + _ -> set.new() + } + do_free(rest, [free, ..acc]) + } + } +} + +pub fn free(refs, previous) { + do_free(list.drop(refs, list.length(previous)), list.reverse(previous)) +} + +fn do_free_map(rest, acc) { + case rest { + [] -> acc + [node, ..rest] -> { + let free = case node { + source.Var(x) -> setx.singleton(x) + source.Fn(x, ref) -> { + let assert Ok(body) = map.get(acc, ref) + set.delete(body, x) + } + source.Let(x, ref_v, ref_t) -> { + let assert Ok(value) = map.get(acc, ref_v) + let assert Ok(then) = map.get(acc, ref_t) + set.union(value, set.delete(then, x)) + } + source.Call(ref_func, ref_arg) -> { + let assert Ok(func) = map.get(acc, ref_func) + let assert Ok(arg) = map.get(acc, ref_arg) + set.union(func, arg) + } + _ -> set.new() + } + do_free_map(rest, map.insert(acc, map.size(acc), free)) + } + } +} + +pub fn free_map(refs, previous) { + // TODO need to be careful on taking new only + do_free_map(list.drop(refs, map.size(previous)), previous) +} + +// // Need single map of substitutions, is this the efficient J algo? +// // Free should be easy in bottom up order assume need value is already present +// // probably not fast in edits to std lib. maybe with only part of record field it would be faster. +// // but if async and cooperative then we can just manage without as needed. +// // TODO have building type stuff happen at startup. try it out in browser + +// TODO test calling the same node twice hits the cach + +pub fn cache_lookup(cache, ref, env) { + use envs <- result.then(map.get(cache, ref)) + + map.get(envs, env) +} + +pub fn cache_update(cache, ref, env, t) { + map.update( + cache, + ref, + fn(cached) { + option.unwrap(cached, map.new()) + |> map.insert(env, t) + }, + ) +} + +// frees can be built lazily as a map +// hash cache can exist for saving to file +pub fn cached( + ref: Int, + source, + frees, + types, + env, + subs: sub.Substitutions, + count, +) { + let assert Ok(free) = list.at(frees, ref) + let required = map.take(env, set.to_list(free)) + case cache_lookup(types, ref, required) { + Ok(t) -> #(t, subs, types) + Error(Nil) -> { + let assert Ok(node) = list.at(source, ref) + let #(t, subs, cache) = case node { + source.Var(x) -> + case map.get(env, x) { + Ok(scheme) -> { + let t = unification.instantiate(scheme, count) + #(t, subs, types) + } + Error(Nil) -> + panic("no var in env need to add errors but return normal type") + } + source.Let(x, value, then) -> { + let #(t1, subs, types) = + cached(value, source, frees, types, env, subs, count) + let scheme = unification.generalise(env, t1) + let env = map.insert(env, x, scheme) + cached(then, source, frees, types, env, subs, count) + } + source.Fn(x, body) -> { + let param = unification.fresh(count) + let env = map.insert(env, x, Scheme([], t.Unbound(param))) + let #(body, subs, types) = + cached(body, source, frees, types, env, subs, count) + let t = t.Fun(t.Unbound(param), t.Closed, body) + #(t, subs, types) + } + source.Call(func, arg) -> { + let ret = unification.fresh(count) + let #(t1, subs, types) = + cached(func, source, frees, types, env, subs, count) + let #(t2, subs, types) = + cached(arg, source, frees, types, env, subs, count) + let subs = case + unification.unify(t.Fun(t2, t.Closed, t.Unbound(ret)), t1, count) + { + Ok(su) -> sub.compose(subs, su) + Error(reason) -> { + io.debug(reason) + subs + } + } + #(t.Unbound(ret), subs, types) + } + source.Integer(_) -> #(t.Integer, subs, types) + source.String(_) -> #(t.Binary, subs, types) + _ -> #(t.Unbound(unification.fresh(count)), subs, types) + } + let cache = cache_update(cache, ref, required, t) + #(t, subs, cache) + } + } +} diff --git a/eyg/src/eyg/incremental/source.gleam b/eyg/src/eyg/incremental/source.gleam new file mode 100644 index 000000000..6085f8a4a --- /dev/null +++ b/eyg/src/eyg/incremental/source.gleam @@ -0,0 +1,204 @@ +import gleam/list +import gleam/map +import gleam/result +import gleam/javascript +import eygir/expression as e + +pub type Expression { + Var(String) + Fn(String, Int) + Let(String, Int, Int) + Call(Int, Int) + Integer(Int) + String(String) + Tail + Cons + Vacant(comment: String) + Empty + Extend(label: String) + Select(label: String) + Overwrite(label: String) + Tag(label: String) + Case(label: String) + NoCases + Perform(label: String) + Handle(label: String) + Builtin(identifier: String) +} + +pub type Source = + map.Map(Int, Expression) + +pub fn do_from_tree(tree, acc) { + case tree { + e.Variable(label) -> #(Var(label), acc) + e.Lambda(label, body) -> { + let #(node, acc) = do_from_tree(body, acc) + let index = list.length(acc) + let acc = [node, ..acc] + #(Fn(label, index), acc) + } + e.Let(label, value, then) -> { + let #(then, acc) = do_from_tree(then, acc) + let then_index = list.length(acc) + let acc = [then, ..acc] + let #(value, acc) = do_from_tree(value, acc) + let value_index = list.length(acc) + let acc = [value, ..acc] + #(Let(label, value_index, then_index), acc) + } + e.Apply(func, arg) -> { + let #(arg, acc) = do_from_tree(arg, acc) + let arg_index = list.length(acc) + let acc = [arg, ..acc] + let #(func, acc) = do_from_tree(func, acc) + let func_index = list.length(acc) + let acc = [func, ..acc] + #(Call(func_index, arg_index), acc) + } + e.Binary(value) -> #(String(value), acc) + e.Integer(value) -> #(Integer(value), acc) + e.Tail -> #(Tail, acc) + e.Cons -> #(Cons, acc) + e.Vacant(comment) -> #(Vacant(comment), acc) + e.Empty -> #(Empty, acc) + e.Extend(label) -> #(Extend(label), acc) + e.Select(label) -> #(Select(label), acc) + e.Overwrite(label) -> #(Overwrite(label), acc) + e.Tag(label) -> #(Tag(label), acc) + e.Case(label) -> #(Case(label), acc) + e.NoCases -> #(NoCases, acc) + e.Perform(label) -> #(Perform(label), acc) + e.Handle(label) -> #(Handle(label), acc) + e.Builtin(identifier) -> #(Builtin(identifier), acc) + } +} + +pub fn from_tree(tree) { + let #(exp, acc) = do_from_tree(tree, []) + let index = list.length(acc) + let source = list.reverse([exp, ..acc]) + #(index, source) +} + +fn next(ref) { + javascript.update_reference(ref, fn(x) { x + 1 }) +} + +fn push(x, ref) { + let #(node, source) = x + let index = next(ref) + let source = map.insert(source, index, node) + #(index, source) +} + +pub fn do_from_tree_map(tree, acc, ref) -> #(Int, map.Map(Int, Expression)) { + case tree { + e.Variable(label) -> #(Var(label), acc) + e.Lambda(label, body) -> { + let #(index, acc) = do_from_tree_map(body, acc, ref) + #(Fn(label, index), acc) + } + e.Let(label, value, then) -> { + let #(then, acc) = do_from_tree_map(then, acc, ref) + let #(value, acc) = do_from_tree_map(value, acc, ref) + #(Let(label, value, then), acc) + } + e.Apply(func, arg) -> { + let #(arg, acc) = do_from_tree_map(arg, acc, ref) + let #(func, acc) = do_from_tree_map(func, acc, ref) + #(Call(func, arg), acc) + } + e.Binary(value) -> #(String(value), acc) + e.Integer(value) -> #(Integer(value), acc) + e.Tail -> #(Tail, acc) + e.Cons -> #(Cons, acc) + e.Vacant(comment) -> #(Vacant(comment), acc) + e.Empty -> #(Empty, acc) + e.Extend(label) -> #(Extend(label), acc) + e.Select(label) -> #(Select(label), acc) + e.Overwrite(label) -> #(Overwrite(label), acc) + e.Tag(label) -> #(Tag(label), acc) + e.Case(label) -> #(Case(label), acc) + e.NoCases -> #(NoCases, acc) + e.Perform(label) -> #(Perform(label), acc) + e.Handle(label) -> #(Handle(label), acc) + e.Builtin(identifier) -> #(Builtin(identifier), acc) + } + |> push(ref) +} + +pub fn from_tree_map(tree) { + let #(index, source) = + do_from_tree_map(tree, map.new(), javascript.make_reference(0)) + #(index, source) +} + +pub fn to_tree(source, root) { + use exp <- result.then(map.get(source, root)) + case exp { + Var(x) -> Ok(e.Variable(x)) + Fn(x, ref) -> { + use exp <- result.then(to_tree(source, ref)) + Ok(e.Lambda(x, exp)) + } + Let(x, ref1, ref2) -> { + use exp1 <- result.then(to_tree(source, ref1)) + use exp2 <- result.then(to_tree(source, ref2)) + Ok(e.Let(x, exp1, exp2)) + } + Call(ref1, ref2) -> { + use exp1 <- result.then(to_tree(source, ref1)) + use exp2 <- result.then(to_tree(source, ref2)) + Ok(e.Apply(exp1, exp2)) + } + String(value) -> Ok(e.Binary(value)) + Integer(value) -> Ok(e.Integer(value)) + Tail -> Ok(e.Tail) + Cons -> Ok(e.Cons) + Vacant(comment) -> Ok(e.Vacant(comment)) + Empty -> Ok(e.Empty) + Extend(label) -> Ok(e.Extend(label)) + Select(label) -> Ok(e.Select(label)) + Overwrite(label) -> Ok(e.Overwrite(label)) + Tag(label) -> Ok(e.Tag(label)) + Case(label) -> Ok(e.Case(label)) + NoCases -> Ok(e.NoCases) + Perform(label) -> Ok(e.Perform(label)) + Handle(label) -> Ok(e.Handle(label)) + Builtin(identifier) -> Ok(e.Builtin(identifier)) + } +} + +pub fn insert(acc, exp) { + let id = map.size(acc) + #(id, map.insert(acc, id, exp)) +} + +fn do_replace(old_id, new_id, zoom, source) { + case zoom { + [] -> Ok(#(new_id, source)) + [next, ..zoom] -> { + use node <- result.then(map.get(source, next)) + let exp = case node { + Let(label, value, then) if value == old_id -> Let(label, new_id, then) + Let(label, value, then) if then == old_id -> Let(label, value, new_id) + Fn(param, body) if body == old_id -> Fn(param, new_id) + Call(func, arg) if func == old_id -> Call(new_id, arg) + Call(func, arg) if arg == old_id -> Call(func, new_id) + _ -> panic("Can't have a path into literal") + } + let new_id = map.size(source) + let source = map.insert(source, new_id, exp) + do_replace(next, new_id, zoom, source) + } + } +} + +pub fn replace(source, cursor, new_id) { + case cursor { + #([], old) -> do_replace(old, new_id, [], source) + #([old, ..zoom], root) -> + do_replace(old, new_id, list.append(zoom, [root]), source) + } +} diff --git a/eyg/src/eyg/incremental/store.gleam b/eyg/src/eyg/incremental/store.gleam new file mode 100644 index 000000000..259b0ed57 --- /dev/null +++ b/eyg/src/eyg/incremental/store.gleam @@ -0,0 +1,150 @@ +import gleam/io +import gleam/list +import gleam/map.{Map} +import gleam/result +import gleam/set.{Set} +import gleam/setx +import gleam/javascript +// TODO source -> ref +import eyg/analysis/typ as t +import eyg/analysis/substitutions as sub +import eyg/analysis/scheme.{Scheme} +import eyg/analysis/unification +import eyg/incremental/source +import eyg/analysis/env +import eyg/incremental/inference +import eyg/incremental/cursor +import plinth/javascript/map as mutable_map + +pub type Store { + Store( + source: Map(Int, source.Expression), + // Not used. do map size instead + source_id_tracker: javascript.Reference(Int), + free: Map(Int, Set(String)), + free_mut: mutable_map.MutableMap(Int, Set(String)), + types: Map(Int, Map(Map(String, Scheme), t.Term)), + substitutions: sub.Substitutions, + counter: javascript.Reference(Int), + ) +} + +pub fn empty() { + Store( + source: map.new(), + source_id_tracker: javascript.make_reference(0), + free: map.new(), + free_mut: mutable_map.new(), + types: map.new(), + substitutions: sub.none(), + counter: javascript.make_reference(0), + ) +} + +pub fn load(store: Store, tree) { + let #(index, source) = + source.do_from_tree_map(tree, store.source, store.source_id_tracker) + #(index, Store(..store, source: source)) +} + +pub fn free(store: Store, root) -> Result(#(Set(String), Store), Nil) { + case map.get(store.free, root) { + Ok(vars) -> Ok(#(vars, store)) + Error(Nil) -> { + use node <- result.then(map.get(store.source, root)) + use #(vars, store) <- result.then(case node { + source.Var(x) -> Ok(#(setx.singleton(x), store)) + source.Fn(x, ref) -> { + use #(vars, store) <- result.then(free(store, ref)) + Ok(#(set.delete(vars, x), store)) + } + source.Let(x, ref_v, ref_t) -> { + use #(value, store) <- result.then(free(store, ref_v)) + use #(then, store) <- result.then(free(store, ref_t)) + let vars = set.union(value, set.delete(then, x)) + Ok(#(vars, store)) + } + source.Call(ref_func, ref_arg) -> { + use #(func, store) <- result.then(free(store, ref_func)) + use #(arg, store) <- result.then(free(store, ref_arg)) + let vars = set.union(func, arg) + Ok(#(vars, store)) + } + _ -> Ok(#(set.new(), store)) + }) + let free = map.insert(store.free, root, vars) + let store = Store(..store, free: free) + Ok(#(vars, store)) + } + } +} + +pub fn free_mut(store: Store, root) -> Result(#(Set(String), Store), Nil) { + case mutable_map.get(store.free_mut, root) { + Ok(vars) -> Ok(#(vars, store)) + Error(Nil) -> { + use node <- result.then(map.get(store.source, root)) + use #(vars, store) <- result.then(case node { + source.Var(x) -> Ok(#(setx.singleton(x), store)) + source.Fn(x, ref) -> { + use #(vars, store) <- result.then(free_mut(store, ref)) + Ok(#(set.delete(vars, x), store)) + } + source.Let(x, ref_v, ref_t) -> { + use #(value, store) <- result.then(free_mut(store, ref_v)) + use #(then, store) <- result.then(free_mut(store, ref_t)) + let vars = set.union(value, set.delete(then, x)) + Ok(#(vars, store)) + } + source.Call(ref_func, ref_arg) -> { + use #(func, store) <- result.then(free_mut(store, ref_func)) + use #(arg, store) <- result.then(free_mut(store, ref_arg)) + let vars = set.union(func, arg) + Ok(#(vars, store)) + } + _ -> Ok(#(set.new(), store)) + }) + let free = mutable_map.set(store.free_mut, root, vars) + // let store = Store(..store, free: free) + Ok(#(vars, store)) + } + } +} + +pub fn cursor(store: Store, root, path) { + cursor.at(path, root, store.source) +} + +// Move to source or cursor + +// could separate pushing one item from fn and pass in new index here +pub fn replace(source, cursor, exp) { + let new_id = map.size(source) + let source = map.insert(source, new_id, exp) + source.replace(source, cursor, new_id) +} + +pub fn focus(store: Store, c) { + map.get(store.source, cursor.inner(c)) +} + +// used for debugging maps. there should be only one reference to a node in a store with a single root. +pub fn ref_group(store: Store) { + let child_refs = + list.flat_map( + map.to_list(store.source), + fn(entry) { + let #(_ref, node) = entry + case node { + // source.Var(String) + source.Fn(_, child) -> [#(child, entry)] + source.Let(_, c1, c2) -> [#(c1, entry), #(c2, entry)] + source.Call(c1, c2) -> [#(c1, entry), #(c2, entry)] + _ -> [] + } + }, + ) + child_refs + |> list.group(fn(x) { x.0 }) + |> map.filter(fn(_, v) { list.length(v) < 1 }) +} diff --git a/eyg/src/node_ffi.js b/eyg/src/node_ffi.js new file mode 100644 index 000000000..5eb020f4d --- /dev/null +++ b/eyg/src/node_ffi.js @@ -0,0 +1,5 @@ +import * as crypto from "node:crypto"; + +export function hash(array) { + return crypto.createHash("sha1").update(array.buffer).digest("hex"); +} diff --git a/eyg/test/atelier/ui_test.gleam b/eyg/test/atelier/ui_test.gleam index 7063f22be..48acbf69f 100644 --- a/eyg/test/atelier/ui_test.gleam +++ b/eyg/test/atelier/ui_test.gleam @@ -1,56 +1,56 @@ -import gleeunit/should -import eygir/expression as e -import atelier/app - -pub fn call_test() { - let source = e.Let("x", e.Binary("initial"), e.Variable("x")) - let initial = app.init(source) - - // update value of let - let #(state, _cmd) = app.select_node(initial, [0]) - let #(state, _cmd) = app.keypress("w", state) - e.Let("x", e.Apply(e.Vacant(""), e.Binary("initial")), e.Variable("x")) - |> should.equal(state.source) - - // update final statement - let #(state, _cmd) = app.select_node(initial, [1]) - let #(state, _cmd) = app.keypress("w", state) - e.Let("x", e.Binary("initial"), e.Apply(e.Vacant(""), e.Variable("x"))) - |> should.equal(state.source) -} - -pub fn insert_parameter_test() { - // test variable - let source = e.Let("_", e.Variable("x"), e.Vacant("")) - let initial = app.init(source) - - let #(state, _cmd) = app.select_node(initial, [0]) - let #(state, _cmd) = app.keypress("i", state) - let assert app.WriteLabel(initial, commit) = state.mode - should.equal(initial, "x") - e.Let("_", e.Variable("foo"), e.Vacant("")) - |> should.equal(commit("foo")) - - // test lambdanested to test step and zip - let source = e.Lambda("x", e.Lambda("y", e.Vacant(""))) - let initial = app.init(source) - - let #(state, _cmd) = app.select_node(initial, [0]) - let #(state, _cmd) = app.keypress("i", state) - let assert app.WriteLabel(initial, commit) = state.mode - should.equal(initial, "y") - e.Lambda("x", e.Lambda("foo", e.Vacant(""))) - |> should.equal(commit("foo")) - - // test let - let source = - e.Let("_", e.Let("x", e.Binary("stuff"), e.Vacant("")), e.Vacant("")) - let initial = app.init(source) - - let #(state, _cmd) = app.select_node(initial, [0]) - let #(state, _cmd) = app.keypress("i", state) - let assert app.WriteLabel(initial, commit) = state.mode - should.equal(initial, "x") - e.Let("_", e.Let("foo", e.Binary("stuff"), e.Vacant("")), e.Vacant("")) - |> should.equal(commit("foo")) -} +// import gleeunit/should +// import eygir/expression as e +// import atelier/app + +// pub fn call_test() { +// let source = e.Let("x", e.Binary("initial"), e.Variable("x")) +// let initial = app.init(source) + +// // update value of let +// let #(state, _cmd) = app.select_node(initial, [0]) +// let #(state, _cmd) = app.keypress("w", state) +// e.Let("x", e.Apply(e.Vacant(""), e.Binary("initial")), e.Variable("x")) +// |> should.equal(state.source) + +// // update final statement +// let #(state, _cmd) = app.select_node(initial, [1]) +// let #(state, _cmd) = app.keypress("w", state) +// e.Let("x", e.Binary("initial"), e.Apply(e.Vacant(""), e.Variable("x"))) +// |> should.equal(state.source) +// } + +// pub fn insert_parameter_test() { +// // test variable +// let source = e.Let("_", e.Variable("x"), e.Vacant("")) +// let initial = app.init(source) + +// let #(state, _cmd) = app.select_node(initial, [0]) +// let #(state, _cmd) = app.keypress("i", state) +// let assert app.WriteLabel(initial, commit) = state.mode +// should.equal(initial, "x") +// e.Let("_", e.Variable("foo"), e.Vacant("")) +// |> should.equal(commit("foo")) + +// // test lambdanested to test step and zip +// let source = e.Lambda("x", e.Lambda("y", e.Vacant(""))) +// let initial = app.init(source) + +// let #(state, _cmd) = app.select_node(initial, [0]) +// let #(state, _cmd) = app.keypress("i", state) +// let assert app.WriteLabel(initial, commit) = state.mode +// should.equal(initial, "y") +// e.Lambda("x", e.Lambda("foo", e.Vacant(""))) +// |> should.equal(commit("foo")) + +// // test let +// let source = +// e.Let("_", e.Let("x", e.Binary("stuff"), e.Vacant("")), e.Vacant("")) +// let initial = app.init(source) + +// let #(state, _cmd) = app.select_node(initial, [0]) +// let #(state, _cmd) = app.keypress("i", state) +// let assert app.WriteLabel(initial, commit) = state.mode +// should.equal(initial, "x") +// e.Let("_", e.Let("foo", e.Binary("stuff"), e.Vacant("")), e.Vacant("")) +// |> should.equal(commit("foo")) +// } diff --git a/eyg/test/examples/algorithm_j_test.gleam b/eyg/test/examples/algorithm_j_test.gleam new file mode 100644 index 000000000..b5ab2d2ed --- /dev/null +++ b/eyg/test/examples/algorithm_j_test.gleam @@ -0,0 +1,93 @@ +import gleam/map.{get, insert as extend} +import gleam/result.{then as try_} + +pub type Literal { + Integer + String +} + +pub type Exp { + Var(String) + App(Exp, Exp) + Abs(String, Exp) + Let(String, Exp, Exp) + Lit(Literal) +} + +pub type Type { + TInt + // TString + Unbound(Int) + Fn(Type, Type) +} + +pub type Scheme = + #(List(Int), Type) + +pub fn fresh(next) { + #(Unbound(next), next + 1) +} + +pub fn instantiate(scheme, next) { + todo +} + +pub fn mono(t) { + #([], t) +} + +pub fn generalise(env, t) { + todo +} + +pub fn unify(t1, t2, s) { + do_unify([#(t1, t2)], s) +} + +// s is a function from var -> t +pub fn do_unify(constraints, s) { + // Have to try and substitute at every point because new substitutions can come into existance + case constraints { + [] -> Ok(s) + [#(Unbound(i), Unbound(j)), ..constraints] -> do_unify(constraints, s) + [#(Unbound(i), t1), ..constraints] | [#(t1, Unbound(i)), ..constraints] -> + case map.get(s, i) { + Ok(t2) -> do_unify([#(t1, t2), ..constraints], s) + Error(Nil) -> do_unify(constraints, map.insert(s, i, t1)) + } + [#(Fn(a1, r1), Fn(a2, r2)), ..cs] -> + do_unify([#(a1, a2), #(r1, r2), ..cs], s) + _ -> Error(Nil) + } +} + +// In exercise a type can be type err +pub fn j(env, exp, sub, next) { + case exp { + Var(x) -> { + use scheme <- try_(get(env, x)) + let #(type_, next) = instantiate(scheme, next) + Ok(#(sub, next, type_)) + } + App(e1, e2) -> { + use #(sub1, next, t1) <- try_(j(env, e1, sub, next)) + use #(sub2, next, t2) <- try_(j(env, e2, sub1, next)) + let #(beta, next) = fresh(next) + use sub3 <- try_(unify(t1, Fn(t2, beta), sub2)) + Ok(#(sub, next, beta)) + } + Abs(x, e1) -> { + let #(beta, next) = fresh(next) + let env1 = extend(env, x, mono(beta)) + use #(sub1, next, t1) <- try_(j(env, e1, sub, next)) + Ok(#(sub1, next, Fn(beta, t1))) + } + Let(x, e1, e2) -> { + use #(sub1, next, t1) <- try_(j(env, e1, sub, next)) + // generalise needs sub applied tot1 + let env1 = extend(env, x, generalise(env, t1)) + use #(sub2, next, t2) <- try_(j(env, e2, sub1, next)) + } + Lit(l) -> todo + } +} diff --git a/eyg/test/examples/j_cont_test.gleam b/eyg/test/examples/j_cont_test.gleam new file mode 100644 index 000000000..45bf87ce8 --- /dev/null +++ b/eyg/test/examples/j_cont_test.gleam @@ -0,0 +1,193 @@ +import gleam/io +import gleam/map.{get, insert as extend} +import gleam/result.{then as try_} + +pub type Literal { + Integer + String +} + +pub type Exp { + Var(String) + App(Exp, Exp) + Abs(String, Exp) + Let(String, Exp, Exp) + Lit(Literal) +} + +pub type Type { + TInt + TString + Unbound(Int) + Fn(Type, Type) + TErr(String) +} + +pub type Scheme = + #(List(Int), Type) + +pub fn fresh(next) { + #(Unbound(next), next + 1) +} + +pub fn instantiate(scheme, next) { + todo +} + +pub fn mono(t) { + #([], t) +} + +pub fn generalise(env, t) { + todo +} + +pub fn unify(t1, t2, s) { + do_unify([#(t1, t2)], s) +} + +// s is a function from var -> t +pub fn do_unify(constraints, s) { + // Have to try and substitute at every point because new substitutions can come into existance + case constraints { + [] -> Ok(s) + [#(Unbound(i), Unbound(j)), ..constraints] -> do_unify(constraints, s) + [#(Unbound(i), t1), ..constraints] | [#(t1, Unbound(i)), ..constraints] -> + case map.get(s, i) { + Ok(t2) -> do_unify([#(t1, t2), ..constraints], s) + Error(Nil) -> do_unify(constraints, map.insert(s, i, t1)) + } + [#(Fn(a1, r1), Fn(a2, r2)), ..cs] -> + do_unify([#(a1, a2), #(r1, r2), ..cs], s) + _ -> Error(Nil) + } +} + +pub type Run { + Done(#(map.Map(Int, Type), Int, #(List(Int), Type))) + Continue( + #(map.Map(Int, Type), Int, #(List(Int), Type)), + fn(#(map.Map(Int, Type), Int, #(List(Int), Type))) -> Run, + ) +} + +// J gi +// In exercise a type can be type err +// If we ignore unification with error +// let a = 1(2) +// let b = a +// let c = a +// +// f(x) +// f String -> Int +// x Error +// Int +// unify(string -> int, fn(err -> beta)) +// int +// I think there is a version of next where we track id -> it replaces path +// incremental id and offset id are not the same thing at all. + +// Follow id of node -> need free and and types in scope + +// let's do j with effects and follow BUT the id's are NOT unique i.e. the types depend on the environment. +// let's try global inference and see if it's faster +pub fn step(env, exp, sub, next, path, k) { + case exp { + Var(x) -> { + let scheme = result.unwrap(get(env, x), #([], TErr("missing var"))) + let #(type_, next) = instantiate(scheme, next) + Continue(#(sub, next, #(path, type_)), k) + } + App(e1, e2) -> { + use #(sub1, next, #(_, t1)) <- step(env, e1, sub, next, [0, ..path]) + use #(sub2, next, #(_, t2)) <- step(env, e2, sub1, next, [1, ..path]) + let #(beta, next) = fresh(next) + // returns t error + let sub3 = case unify(t1, Fn(t2, beta), sub2) { + Ok(sub3) -> sub3 + // TODO record error + Error(_) -> sub + } + // let assert Ok(sub3) = + Continue(#(sub3, next, #(path, beta)), k) + } + Abs(x, e1) -> { + let #(beta, next) = fresh(next) + let env1 = extend(env, x, mono(beta)) + use #(sub1, next, #(_, t1)) <- step(env1, e1, sub, next, [0, ..path]) + Continue(#(sub1, next, #(path, Fn(beta, t1))), k) + } + Let(x, e1, e2) -> { + use #(sub1, next, #(_, t1)) <- step(env, e1, sub, next, [0, ..path]) + // generalise needs sub applied tot1 + let env1 = extend(env, x, generalise(env, t1)) + use #(sub2, next, #(_, t2)) <- step(env, e2, sub1, next, [1, ..path]) + Continue(#(sub2, next, #(path, t2)), k) + } + Lit(Integer) -> Continue(#(sub, next, #(path, TInt)), k) + Lit(String) -> Continue(#(sub, next, #(path, TString)), k) + } +} + +pub fn loop(run) { + case run { + Done(r) -> r + Continue(r, k) -> { + io.debug(r) + loop(k(r)) + } + } +} + +pub fn j(env, exp, sub, next) { + loop(step(env, exp, sub, next, [], Done)) +} + +pub fn demo(items, k) { + case items { + [] -> k("done") + [g, ..rest] -> + demo( + rest, + fn(x) { + // io.debug(g) + g + 1 + k(x) + }, + ) + } +} + +pub fn demo2(items, k) { + case items { + [] -> k("done") + [g, ..rest] -> { + use x <- demo2(rest) + // io.debug(g) + g + 1 + k(x) + } + } +} + +// use twice makes reference to demo3, that blows the stack. Document this +// TODO plinth library +pub fn demo3(items, k) { + case items { + [] -> k("done") + [g, ..rest] -> { + use x <- demo3(rest) + use y <- demo3(rest) + + // io.debug(g) + g + 1 + k(x) + } + } +} + +pub fn order_test() { + j(map.new(), App(Lit(Integer), Lit(String)), map.new(), 0) + |> io.debug + todo +} diff --git a/eyg/test/eyg/analysis/jm/infer_test.gleam b/eyg/test/eyg/analysis/jm/infer_test.gleam new file mode 100644 index 000000000..656416de5 --- /dev/null +++ b/eyg/test/eyg/analysis/jm/infer_test.gleam @@ -0,0 +1,55 @@ +import gleam/io +import gleam/map +import gleam/set +import gleam/setx +import eygir/expression as e +import eyg/analysis/typ.{ftv} as t +import eyg/analysis/jm/type_ +import eyg/analysis/env +import eyg/analysis/inference.{infer, type_of} +// top level analysis +import eyg/analysis/scheme.{Scheme} +import eyg/analysis/unification +import gleeunit/should +import eyg/incremental/store +import eyg/analysis/jm/incremental as jm +import eyg/analysis/jm/type_ as jmt +import eyg/analysis/jm/error as jm_error + +fn call2(f, a, b) { + e.Apply(e.Apply(f, a), b) +} + +pub fn example_test() { + let exp = + e.Let( + "equal", + e.Builtin("equal"), + e.Let( + "_", + call2(e.Variable("equal"), e.Binary(""), e.Binary("")), + e.Let( + "_", + call2(e.Variable("equal"), e.Integer(1), e.Integer(1)), + e.Empty, + ), + ), + ) + + let #(root, store) = store.load(store.empty(), exp) + + let sub = map.new() + let next = 0 + let env = map.new() + let source = store.source + let ref = root + let types = map.new() + + io.debug("loaded") + let #(sub, _next, types) = + jm.infer(sub, next, env, source, ref, jmt.Var(-1), jmt.Var(-2), types) + map.get(types, root) + // |> io.debug + // io.debug(types |> map.to_list) +} +// TODO try and have test for recursive types in rows/effects diff --git a/eyg/test/eyg/analysis/typing_test.gleam b/eyg/test/eyg/analysis/typing_test.gleam index 7e52f2012..35d4b9aa6 100644 --- a/eyg/test/eyg/analysis/typing_test.gleam +++ b/eyg/test/eyg/analysis/typing_test.gleam @@ -1,14 +1,20 @@ +import gleam/io import gleam/map import gleam/set import gleam/setx import eygir/expression as e import eyg/analysis/typ.{ftv} as t +import eyg/analysis/jm/type_ import eyg/analysis/env import eyg/analysis/inference.{infer, type_of} // top level analysis import eyg/analysis/scheme.{Scheme} import eyg/analysis/unification import gleeunit/should +import eyg/incremental/store +import eyg/analysis/jm/incremental as jm +import eyg/analysis/jm/type_ as jmt +import eyg/analysis/jm/error as jm_error pub fn resolve(inf: inference.Infered, typ) { unification.resolve(inf.substitutions, typ) @@ -55,6 +61,37 @@ pub fn free_type_variables_test() { |> should.equal(set.from_list([t.Term(1), t.Term(2), t.Effect(3)])) } +fn jm(exp, type_, eff) { + let #(root, store) = store.load(store.empty(), exp) + + let sub = map.new() + let next = 0 + let env = map.new() + let source = store.source + let ref = root + let types = map.new() + + let #(sub, _next, types) = + jm.infer(sub, next, env, source, ref, type_, eff, types) + case map.get(types, root) { + Ok(Ok(t)) -> Ok(jmt.resolve(t, sub)) + Ok(Error(reason)) -> Error(reason) + // DOes panic expression print message + Error(_) -> { + io.debug(root) + io.debug( + source + |> map.to_list, + ) + io.debug( + types + |> map.to_list, + ) + todo("no typr") + } + } +} + // Primitive pub fn binary_test() { let exp = e.Binary("hi") @@ -71,11 +108,17 @@ pub fn binary_test() { let sub = infer(env, exp, typ, eff) let assert t.Binary = resolve(sub, typ) let assert Ok(t.Binary) = type_of(sub, []) + let assert Ok(type_.String) = jm(exp, type_.Var(-1), type_.Empty) // incorrect type let typ = t.Integer let sub = infer(env, exp, typ, eff) let assert Error(_) = type_of(sub, []) + let assert Error(#( + jm_error.TypeMismatch(jmt.Integer, jmt.String), + jmt.Integer, + jmt.String, + )) = jm(exp, type_.Integer, type_.Empty) } pub fn integer_test() { @@ -128,6 +171,8 @@ pub fn empty_list_test() { let typ = t.Binary let sub = infer(env, exp, typ, eff) let assert Error(_) = type_of(sub, []) + let assert Ok(type_.LinkedList(type_.Var(_))) = + jm(exp, type_.Var(-1), type_.Empty) } pub fn primitive_list_test() { @@ -138,6 +183,10 @@ pub fn primitive_list_test() { let eff = t.Closed let sub = infer(env, exp, typ, eff) let assert Ok(t.LinkedList(t.Binary)) = type_of(sub, []) + let assert Ok(type_.LinkedList(type_.Integer)) = + jm(exp, type_.Var(-1), type_.Empty) + // THere is an error missing and the principle is wrong + let assert Ok(t.Fun(t.LinkedList(t.Binary), t.Closed, t.LinkedList(t.Binary))) = type_of(sub, [0]) let assert Ok(t.LinkedList(t.Binary)) = type_of(sub, [1]) @@ -161,6 +210,7 @@ pub fn variables_test() { let typ = t.Unbound(1) let sub = infer(env, exp, typ, eff) let assert t.Binary = resolve(sub, typ) + let assert Ok(type_.String) = jm(exp, type_.Var(-1), type_.Empty) let assert Ok(t.Binary) = type_of(sub, [0]) let assert Ok(t.Binary) = type_of(sub, [1]) } @@ -177,6 +227,8 @@ pub fn function_test() { let typ = t.Unbound(-1) let sub = infer(env, exp, typ, eff) let assert t.Fun(t.Unbound(_), t.Open(_), t.Binary) = resolve(sub, typ) + let assert Ok(type_.Fun(type_.Var(_), type_.Var(_), type_.String)) = + jm(exp, type_.Var(-1), type_.Empty) } pub fn pure_function_test() { @@ -190,6 +242,9 @@ pub fn pure_function_test() { should.equal(x, y) let assert Ok(t.Unbound(z)) = type_of(sub, [0]) should.equal(x, z) + let assert Ok(type_.Fun(type_.Var(x), type_.Var(_), type_.Var(y))) = + jm(exp, type_.Var(-1), type_.Empty) + should.equal(x, y) } pub fn pure_function_call_test() { @@ -204,6 +259,7 @@ pub fn pure_function_call_test() { let typ = t.Unbound(-1) let sub = infer(env, exp, typ, eff) let assert t.Binary = resolve(sub, typ) + let assert Ok(type_.String) = jm(exp, type_.Var(-1), type_.Empty) } // call generic could be a test row(a = id(Int) b = id(Int)) @@ -229,6 +285,12 @@ pub fn select_test() { resolve(sub, typ) should.equal(a, b) should.equal(l, "foo") + let assert Ok(type_.Fun( + type_.Record(type_.RowExtend("foo", type_.Var(x), type_.Var(_y))), + type_.Var(_z), + type_.Var(a), + )) = jm(exp, type_.Var(-1), type_.Empty) + should.equal(x, a) let exp = e.Apply(exp, e.Variable("x")) let env = env.empty() @@ -261,6 +323,14 @@ pub fn combine_select_test() { should.not_equal(x, y) let assert Ok(t.Unbound(z)) = field(row, "bar") should.equal(x, z) + let exp = e.Lambda("x", exp) + // TODO field fn that excepts Record only type_.field + // Proper double use in select statement test + // slim down saved.json + // All documentation of type systems in "mylang" + let assert Ok(type_.String) = + jm(exp, type_.Var(-1), type_.Empty) + |> io.debug } // Unions @@ -322,6 +392,11 @@ pub fn collect_effects_test() { let assert Ok(#(t.Unbound(lift2), t.Unbound(ret2))) = field(raised, "Log") should.equal(ret1, lift2) should.equal(ret2, final) + + let exp = e.Lambda("_", exp) + let assert Ok(type_.String) = + jm(exp, type_.Var(-1), type_.Empty) + |> io.debug } pub fn anony_test() { diff --git a/eyg/test/eyg/incremental/store_test.gleam b/eyg/test/eyg/incremental/store_test.gleam new file mode 100644 index 000000000..29f081f7b --- /dev/null +++ b/eyg/test/eyg/incremental/store_test.gleam @@ -0,0 +1,227 @@ +// import gleam/io +// import gleam/map +// import gleam/set +// import eygir/expression as e +// import eyg/analysis/typ as t +// import eyg/analysis/inference +// import eyg/incremental/source +// import eyg/incremental/store +// import gleeunit/should + +// // TODO printing map in node +// // TODO binary-size in JS match + +// pub fn literal_test() { +// let s = store.empty() + +// let tree = e.Binary("hello") +// let #(ref_binary, s) = store.load(s, tree) +// should.equal(ref_binary, 0) +// // should.equal(store.tree(s, ref_binary), Ok(tree)) +// let assert Ok(#(free, s)) = store.free(s, ref_binary) +// should.equal(map.size(s.free), 1) +// should.equal(free, set.new()) +// let assert Ok(#(t, s)) = store.type_(s, ref_binary) +// should.equal(map.size(s.types), 1) +// should.equal(t, t.Binary) + +// let #(ref_integer, s) = store.load(s, e.Integer(5)) +// should.equal(ref_integer, 1) +// let assert Ok(#(free, s)) = store.free(s, ref_integer) +// should.equal(map.size(s.free), 2) +// should.equal(free, set.new()) +// let assert Ok(#(t, s)) = store.type_(s, ref_integer) +// should.equal(map.size(s.types), 2) +// should.equal(t, t.Integer) +// } + +// pub fn function_unification_test() { +// let s = store.empty() + +// let tree = e.Apply(e.Lambda("x", e.Variable("x")), e.Binary("hey")) +// let #(root, s) = store.load(s, tree) +// should.equal(root, 3) +// should.equal(map.size(s.source), 4) + +// let assert Ok(#(t, s)) = store.type_(s, root) +// should.equal(t, t.Binary) +// should.equal(map.size(s.free), 4) +// should.equal(map.size(s.types), 4) + +// let assert Ok(c) = store.cursor(s, root, [1]) +// let assert Ok(node) = store.focus(s, c) +// should.equal(node, source.String("hey")) +// let assert Ok(#(root1, s)) = store.replace(s, c, source.Integer(10)) +// should.equal(map.size(s.source), 6) + +// let assert Ok(c) = store.cursor(s, root, [0, 0]) +// let assert Ok(node) = store.focus(s, c) +// should.equal(node, source.Var("x")) +// let assert Ok(#(root2, s)) = store.replace(s, c, source.Empty) +// should.equal(map.size(s.source), 9) +// // source increase by path length + 1 +// // free and types are lazy so stay at 4 +// should.equal(map.size(s.free), 4) +// should.equal(map.size(s.types), 4) + +// let assert Ok(#(t, s)) = store.type_(s, root1) +// should.equal(t, t.Integer) +// let assert Ok(#(t, s)) = store.type_(s, root2) +// should.equal(t, t.unit) + +// should.equal(map.size(s.free), 9) +// should.equal(map.size(s.types), 9) +// } + +// pub fn let_literal_test() { +// let s = store.empty() + +// let tree = e.Let("x", e.Binary("hey"), e.Variable("x")) +// let #(root, s) = store.load(s, tree) +// should.equal(root, 2) +// should.equal(map.size(s.source), 3) + +// let assert Ok(#(t, s)) = store.type_(s, root) +// should.equal(t, t.Binary) +// should.equal(map.size(s.free), 3) +// should.equal(map.size(s.types), 3) + +// let assert Ok(c) = store.cursor(s, root, [0]) +// let assert Ok(node) = store.focus(s, c) +// should.equal(node, source.String("hey")) +// let assert Ok(#(root1, s)) = store.replace(s, c, source.Integer(10)) +// should.equal(map.size(s.source), 5) + +// let assert Ok(c) = store.cursor(s, root, [1]) +// let assert Ok(node) = store.focus(s, c) +// should.equal(node, source.Var("x")) +// let assert Ok(#(root2, s)) = store.replace(s, c, source.Empty) +// should.equal(map.size(s.source), 7) +// // source increase by path length + 1 +// // free and types are lazy so stay at previous value +// should.equal(map.size(s.free), 3) +// should.equal(map.size(s.types), 3) + +// let assert Ok(#(t, s)) = store.type_(s, root1) +// should.equal(t, t.Integer) +// let assert Ok(#(t, s)) = store.type_(s, root2) +// should.equal(t, t.unit) + +// should.equal(map.size(s.free), 7) +// should.equal(map.size(s.types), 7) +// } + +// pub fn fn_poly_test() { +// let s = store.empty() + +// let tree = +// e.Let( +// "id", +// e.Lambda("x", e.Variable("x")), +// e.Apply(e.Variable("id"), e.Integer(10)), +// ) +// let #(root, s) = store.load(s, tree) +// should.equal(root, 5) +// should.equal(map.size(s.source), 6) + +// let assert Ok(#(t, s)) = store.type_(s, root) +// should.equal(t, t.Integer) +// should.equal(map.size(s.free), 6) +// should.equal(map.size(s.types), 6) +// } + +// pub fn nested_fn_test() { +// let s = store.empty() + +// let tree = e.Lambda("x", e.Lambda("y", e.Empty)) +// let #(root, s) = store.load(s, tree) +// should.equal(root, 2) +// should.equal(map.size(s.source), 3) + +// let assert Ok(#(t, s)) = store.type_(s, root) +// should.equal( +// t, +// t.Fun( +// t.Unbound(0), +// t.Closed, +// t.Fun(t.Unbound(1), t.Closed, t.Record(t.Closed)), +// ), +// ) +// should.equal(map.size(s.free), 3) +// should.equal(map.size(s.types), 3) + +// let assert Ok(c) = store.cursor(s, root, [0, 0]) +// let assert Ok(node) = store.focus(s, c) +// should.equal(node, source.Empty) +// let assert Ok(#(root1, s)) = store.replace(s, c, source.Integer(10)) +// should.equal(map.size(s.source), 6) + +// let assert Ok(node) = store.focus(s, c) +// // same cursor points to same item, store replace gives new root +// should.equal(node, source.Empty) +// let assert Ok(#(root2, s)) = store.replace(s, c, source.Var("y")) +// should.equal(map.size(s.source), 9) +// // source increase by path length + 1 +// // free and types are lazy so stay at 4 +// should.equal(map.size(s.free), 3) +// should.equal(map.size(s.types), 3) + +// let assert Ok(#(t, s)) = store.type_(s, root1) +// should.equal( +// t, +// t.Fun(t.Unbound(2), t.Closed, t.Fun(t.Unbound(3), t.Closed, t.Integer)), +// ) +// let assert Ok(#(t, s)) = store.type_(s, root2) +// should.equal( +// t, +// t.Fun(t.Unbound(4), t.Closed, t.Fun(t.Unbound(5), t.Closed, t.Unbound(5))), +// ) + +// should.equal(map.size(s.free), 9) +// should.equal(map.size(s.types), 9) +// } + +// pub fn branched_apply_test() { +// let s = store.empty() + +// let tree = +// e.Let( +// "id", +// e.Lambda("x", e.Variable("x")), +// e.Apply( +// e.Apply(e.Variable("id"), e.Variable("id")), +// e.Apply(e.Variable("id"), e.Integer(1)), +// ), +// ) + +// let #(root, s) = store.load(s, tree) +// should.equal(root, 9) +// should.equal(map.size(s.source), 10) + +// let assert Ok(c) = store.cursor(s, root, []) +// let assert Ok(node) = store.focus(s, c) +// should.equal(node, source.Let("id", 8, 6)) +// let assert Ok(#(vars, s)) = store.free(s, root) +// should.equal(vars, set.new()) +// should.equal(map.size(s.free), 10) +// should.equal(map.size(s.types), 0) + +// let assert Ok(#(t, s)) = store.type_(s, root) +// should.equal(map.size(s.types), 10) +// io.debug( +// s.substitutions.terms +// |> map.to_list, +// ) + +// let sub = inference.infer(map.new(), tree, t.Unbound(-1), t.Open(-2)) +// io.debug(sub.substitutions.terms |> map.to_list) + +// inference.sound(sub) +// |> should.equal(Ok(Nil)) + +// inference.type_of(sub, []) +// |> io.debug + +// should.equal(t, t.Integer) +// panic +// } diff --git a/eyg/test/hash_test.gleam b/eyg/test/hash_test.gleam new file mode 100644 index 000000000..0638915f4 --- /dev/null +++ b/eyg/test/hash_test.gleam @@ -0,0 +1,113 @@ +import gleam/bit_string +import gleam/io +import gleam/result +import eygir/expression as e +import gleeunit/should + +pub fn encode_string(label) { + let binary = bit_string.from_string(label) + <> +} + +pub fn linear(source) { + case source { + e.Variable(label) -> <<1, encode_string(label):bit_string>> + e.Lambda(label, body) -> << + 2, + encode_string(label):bit_string, + linear(body):bit_string, + >> + e.Apply(func, arg) -> <<3, linear(func):bit_string, linear(arg):bit_string>> + e.Let(label, value, then) -> << + 4, + encode_string(label):bit_string, + linear(value):bit_string, + linear(then):bit_string, + >> + e.Integer(value) -> <<5, value:32>> + _ -> panic + } +} + +fn decode_label(x, rest) { + use label <- result.then(bit_string.slice(rest, 0, x)) + use rest <- result.then(bit_string.slice( + rest, + x, + bit_string.byte_size(rest) - x, + )) + use label <- result.then(bit_string.to_string(label)) + Ok(#(label, rest)) +} + +pub fn decode(bytes) { + case bytes { + <<1, x, rest:binary>> -> { + use #(label, rest) <- result.then(decode_label(x, rest)) + Ok(#(e.Variable(label), rest)) + } + <<4, x, rest:binary>> -> { + io.debug("matched") + use #(label, rest) <- result.then(decode_label(x, rest)) + use #(value, rest) <- result.then(decode(rest)) + use #(then, rest) <- result.then(decode(rest)) + Ok(#(e.Let(label, value, then), rest)) + } + <<5, value:32, rest:binary>> -> Ok(#(e.Integer(value), rest)) + _ -> { + io.debug(bytes) + panic("some bytes") + } + } +} + +pub external fn log(a) -> Nil = + "" "console.log" + +// hash and digest +external fn hash(BitString) -> String = + "./node_ffi.js" "hash" + +pub fn gather_hash(source) { + case source { + <<1, x, rest:binary>> -> { + use part <- result.then(bit_string.slice(rest, 0, x)) + Ok(hash(<<1, x, part:bit_string>>)) + } + + // TODO need a pop function that doesn't turn to string, i.e.utf16 on JS + <<4, x, rest:binary>> -> { + io.debug("matched") + use #(label, rest) <- result.then(decode_label(x, rest)) + use #(value, rest) <- result.then(decode(rest)) + use #(then, rest) <- result.then(decode(rest)) + Ok(#(e.Let(label, value, then), rest)) + |> io.debug + panic("not done") + } + } + // Ok(#(e.Variable(label), rest)) + + // <<4, x, rest:binary>> -> { + // io.debug("matched") + // use #(label, rest) <- result.then(decode_label(x, rest)) + // use #(value, rest) <- result.then(decode(rest)) + // use #(then, rest) <- result.then(decode(rest)) + // Ok(#(e.Let(label, value, then), rest)) + // } + // <<5, value:32, rest:binary>> -> Ok(#(e.Integer(value), rest)) + // _ -> { + // io.debug(bytes) + // todo("some bytes") + // } +} + +pub fn round_trip_test() -> Nil { + let tree = e.Let("xyz", e.Integer(5), e.Variable("xyz")) + let source = linear(tree) + decode(source) + |> should.equal(Ok(#(tree, <<>>))) + // gather_hash(<<1, 1, "ab":utf8>>) + // gather_hash(source) + // TODO not really useful +} diff --git a/notes/README.md b/notes/README.md new file mode 100644 index 000000000..e5c2c9018 --- /dev/null +++ b/notes/README.md @@ -0,0 +1,60 @@ +# Notes + +## Type Inference + + +### Judgements + +### Algorithms + +Simple Lambda calculus is easily type checked. +Most of the below algorithms handle extension of at least let polymorphism + + +#### Algorithm W +Explained in Hindley Milner Paper + +Uses composition of substitutions + +#### Algorithm J +Explained in Hindley Milner Paper + +Uses a single global substitution. The global substitution doesn't have to + +### Algorithm M +Adds expected type as argument to algorithm. +This get's results in different orders + +#### Separate constraint collection from solving + +Does Wand fit into this? + +## Language extensions + +### Recursive functions +#### Fix point +Easy to add is just a function with the required type. +This type is not constructable within the lambda calculus + +#### Let Rec + +Every Let could test to see if let rec is used. + +#### Extensible Records and Unions + +using Row types + +#### Effect types + +- [ ] How do we get polymorphic functions in effect types. +Can we use these to compile builtins. + +## Incremental type checking + +J with early return is an option + +Unison hashes only terms without Free variables +There is the other paper that does both but building up the whole tree is tricky because the env at any given point depends on the tree so can't be cached through modifications + +### Differential Datalog + diff --git a/notes/unison.md b/notes/unison.md new file mode 100644 index 000000000..fa911b0cc --- /dev/null +++ b/notes/unison.md @@ -0,0 +1,18 @@ +# Unison + + +Makes use of Abstract Binding Trees. https://semantic-domain.blogspot.com/2015/03/abstract-binding-trees.html + + +unison-hashing-v2/src/Unison/Hashing/V2 +defines all the terms in the tree an uses reference utils to do hashing + +```haskell +hashed $ hash b == hashed (hash b) +``` + +Once in the hash' function. Var is De Brijun index so int64 + + +Steps the compiler goes through +https://github.com/unisonweb/unison/blob/477371ba97a019fe36294a76faed52190ef29d75/parser-typechecker/src/Unison/Runtime/docs.markdown