From 05942b407169da9f0fcf97437cd937b9fb15cd1f Mon Sep 17 00:00:00 2001 From: Zack Eisbach Date: Sun, 29 Mar 2026 16:48:32 -0400 Subject: [PATCH 1/6] draft: stash docstrings in ValueBind --- lang/src/arr/compiler/ast-util.arr | 13 +++++++ lang/src/arr/compiler/compile-structs.arr | 4 ++- lang/src/arr/compiler/desugar.arr | 4 +-- lang/src/arr/compiler/resolve-scope.arr | 43 +++++++++++++++-------- 4 files changed, 47 insertions(+), 17 deletions(-) diff --git a/lang/src/arr/compiler/ast-util.arr b/lang/src/arr/compiler/ast-util.arr index 7853276ef..f31b0cd84 100644 --- a/lang/src/arr/compiler/ast-util.arr +++ b/lang/src/arr/compiler/ast-util.arr @@ -1546,3 +1546,16 @@ fun get-typed-provides(resolved, typed :: TCS.Typed, uri :: URI, compile-env :: end end end + +fun get-doc-string(expr :: A.Expr) -> String: + # empty string sounds bad but we already are parsing missing docstrings + # into the empty string, so we have to case anyways... + doc: "extracts the docstring if one exists; empty string otherwise" + cases(A.Expr) expr: + | s-lam(_, _, _, _, _, doc, _, _, _, _) => doc + | s-fun(_, _, _, _, _, _, doc, _, _, _, _) => doc + | s-method(_, _, _, _, _, doc, _, _, _, _) => doc + | s-method-field(_, _, _, _, _, doc, _, _, _, _) => doc + | else => "" + end +end diff --git a/lang/src/arr/compiler/compile-structs.arr b/lang/src/arr/compiler/compile-structs.arr index 88d11f8f0..30b97fc2f 100644 --- a/lang/src/arr/compiler/compile-structs.arr +++ b/lang/src/arr/compiler/compile-structs.arr @@ -99,7 +99,9 @@ data ValueBind: origin :: BindOrigin, binder :: ValueBinder, atom :: A.Name, - ann :: A.Ann) + ann :: A.Ann, + # the (possible empty) doc string for hovering over this (value) binding + doc :: String) end data TypeBinder: diff --git a/lang/src/arr/compiler/desugar.arr b/lang/src/arr/compiler/desugar.arr index ac785f280..f965a2901 100644 --- a/lang/src/arr/compiler/desugar.arr +++ b/lang/src/arr/compiler/desugar.arr @@ -139,13 +139,13 @@ end fun mk-id-ann(loc, base, ann) block: a = names.make-atom(loc, base) - generated-binds.set-now(a.key(), C.value-bind(C.bo-local(loc, a), C.vb-let, a, ann)) + generated-binds.set-now(a.key(), C.value-bind(C.bo-local(loc, a), C.vb-let, a, ann, "")) { id: a, id-b: A.s-bind(loc, false, a, ann), id-e: A.s-id(loc, a) } end fun mk-id-var-ann(loc, base, ann) block: a = names.make-atom(loc, base) - generated-binds.set-now(a.key(), C.value-bind(C.bo-local(loc, a), C.vb-var, a, ann)) + generated-binds.set-now(a.key(), C.value-bind(C.bo-local(loc, a), C.vb-var, a, ann, "")) { id: a, id-b: A.s-bind(loc, false, a, ann), id-e: A.s-id-var(loc, a) } end diff --git a/lang/src/arr/compiler/resolve-scope.arr b/lang/src/arr/compiler/resolve-scope.arr index 5e076d493..ae5698a07 100644 --- a/lang/src/arr/compiler/resolve-scope.arr +++ b/lang/src/arr/compiler/resolve-scope.arr @@ -769,12 +769,13 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com | some(shadow val-info) => cases(C.ValueExport) val-info block: | v-var(_, t) => - b = C.value-bind(C.bo-global(some(origin), uri-of-definition, origin.original-name), C.vb-var, names.s-global(A.dummy-loc, name), A.a-blank) + # ZACK TODO: what should we do with vars here?? + b = C.value-bind(C.bo-global(some(origin), uri-of-definition, origin.original-name), C.vb-var, names.s-global(A.dummy-loc, name), A.a-blank, "") bindings.set-now(names.s-global(A.dummy-loc, name).key(), b) acc.set-now(name, b) | else => # TODO(joe): Good place to add _location_ to valueexport to report errs better - b = C.value-bind(C.bo-global(some(origin), uri-of-definition, origin.original-name), C.vb-let, names.s-global(A.dummy-loc, name), A.a-blank) + b = C.value-bind(C.bo-global(some(origin), uri-of-definition, origin.original-name), C.vb-let, names.s-global(A.dummy-loc, name), A.a-blank, "IMPORT PLACEHOLDER") bindings.set-now(names.s-global(A.dummy-loc, name).key(), b) acc.set-now(name, b) end @@ -813,8 +814,14 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com # TODO(joe): I think that b.b.ann.visit below could be wrong if # a letrec'd ID is used in a refinement within the same letrec, # so state may be necessary here + atom-env = make-atom-for(b.b.id, b.b.shadows, env, bindings, - C.value-bind(C.bo-local(b.l, b.b.id), C.vb-letrec, _, b.b.ann.visit(visitor))) + C.value-bind( + C.bo-local(b.l, b.b.id), + C.vb-letrec, + _, + b.b.ann.visit(visitor), + U.get-doc-string(b.value))) { atom-env.env; link(atom-env.atom, atoms) } end new-visitor = visitor.{env: env} @@ -864,7 +871,7 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com fun handle-column-binds(column-binds :: A.ColumnBinds, visitor): env-and-binds = for fold(acc from { env: visitor.env, cbs: [list: ] }, cb from column-binds.binds): atom-env = make-atom-for(cb.id, cb.shadows, acc.env, bindings, - C.value-bind(C.bo-local(cb.l, cb.id), C.vb-let, _, cb.ann.visit(visitor))) + C.value-bind(C.bo-local(cb.l, cb.id), C.vb-let, _, cb.ann.visit(visitor), "")) new-cb = A.s-bind(cb.l, cb.shadows, atom-env.atom, cb.ann.visit(visitor.{env: acc.env})) { env: atom-env.env, cbs: link(new-cb, acc.cbs) } end @@ -878,6 +885,7 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com "$included-" + to-string(include-counter) end + # ZACK: revisit like above for module related stuff... fun add-value-name(l, imp-loc, env, vname, as-name, mod-info): maybe-value-export = mod-info.values.get(vname.toname()) cases(Option) maybe-value-export block: @@ -890,7 +898,11 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com | else => C.vb-let end atom-env = make-import-atom-for(as-name, value-export.origin.uri-of-definition, env, bindings, - C.value-bind(C.bo-module(as-name.l, value-export.origin.definition-bind-site, value-export.origin.uri-of-definition, value-export.origin.original-name), vbinder, _, A.a-any(vname.l))) + C.value-bind(C.bo-module(as-name.l, + value-export.origin.definition-bind-site, + value-export.origin.uri-of-definition, + value-export.origin.original-name), + vbinder, _, A.a-any(vname.l), "VALUE ENV PLACEHOLDER")) atom-env.env end end @@ -1474,8 +1486,9 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com # TODO(joe): What should the TypeBindTyp be here? atom-env-t = make-atom-for(name, false, te, type-bindings, C.type-bind(C.bo-local(l2, name), C.tb-type-let, _, C.tb-none)) + # ZACK TODO: wtf is this?? atom-env = make-atom-for(tname, false, e, bindings, - C.value-bind(C.bo-local(l2, tname), C.vb-let, _, A.a-blank)) + C.value-bind(C.bo-local(l2, tname), C.vb-let, _, A.a-blank, "ZACK what this")) new-bind = A.s-newtype-bind(l2, atom-env-t.atom, atom-env.atom) { atom-env.env; atom-env-t.env; link(new-bind, bs) } end @@ -1490,7 +1503,7 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com | s-let-bind(l2, bind, expr) => visited-ann = bind.ann.visit(self.{env: e}) atom-env = make-atom-for(bind.id, bind.shadows, e, bindings, - C.value-bind(C.bo-local(l2, bind.id), C.vb-let, _, visited-ann)) + C.value-bind(C.bo-local(l2, bind.id), C.vb-let, _, visited-ann, U.get-doc-string(expr))) visit-expr = expr.visit(self.{env: e}) new-bind = A.s-let-bind(l2, A.s-bind(l2, bind.shadows, atom-env.atom, visited-ann), visit-expr) { @@ -1501,7 +1514,8 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com | s-var-bind(l2, bind, expr) => visited-ann = bind.ann.visit(self.{env: e}) atom-env = make-atom-for(bind.id, bind.shadows, e, bindings, - C.value-bind(C.bo-local(l2, bind.id), C.vb-var, _, visited-ann)) + # ZACK TODO: we can't do anything here right... + C.value-bind(C.bo-local(l2, bind.id), C.vb-var, _, visited-ann, "")) visit-expr = expr.visit(self.{env: e}) new-bind = A.s-var-bind(l2, A.s-bind(l2, bind.shadows, atom-env.atom, visited-ann), visit-expr) { @@ -1526,7 +1540,7 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com cases(A.ForBind) fb block: | s-for-bind(l2, bind, val) => atom-env = make-atom-for(bind.id, bind.shadows, env, bindings, - C.value-bind(C.bo-local(l2, bind.id), C.vb-let, _, bind.ann.visit(self))) + C.value-bind(C.bo-local(l2, bind.id), C.vb-let, _, bind.ann.visit(self), "")) new-bind = A.s-bind(bind.l, bind.shadows, atom-env.atom, bind.ann.visit(self.{env: env})) visit-val = val.visit(self) new-fb = A.s-for-bind(l2, new-bind, visit-val) @@ -1539,7 +1553,7 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com {env; atoms} = for fold(acc from { self.env; empty }, a from args.map(_.bind)): {env; atoms} = acc atom-env = make-atom-for(a.id, a.shadows, env, bindings, - C.value-bind(C.bo-local(a.l, a.id), C.vb-let, _, a.ann.visit(self))) + C.value-bind(C.bo-local(a.l, a.id), C.vb-let, _, a.ann.visit(self), "")) { atom-env.env; link(atom-env.atom, atoms) } end new-args = for map2(a from args, at from atoms.reverse()): @@ -1580,7 +1594,7 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com {env; atoms} = for fold(acc from { with-params.env; empty }, a from args): {env; atoms} = acc atom-env = make-atom-for(a.id, a.shadows, env, bindings, - C.value-bind(C.bo-local(a.l, a.id), C.vb-let, _, a.ann.visit(with-params))) + C.value-bind(C.bo-local(a.l, a.id), C.vb-let, _, a.ann.visit(with-params), "")) { atom-env.env; link(atom-env.atom, atoms) } end new-args = for map2(a from args, at from atoms.reverse()): @@ -1608,7 +1622,7 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com {env; atoms} = for fold(acc from { with-params.env; empty }, a from args): {env; atoms} = acc atom-env = make-atom-for(a.id, a.shadows, env, bindings, - C.value-bind(C.bo-local(a.l, a.id), C.vb-let, _, a.ann.visit(with-params))) + C.value-bind(C.bo-local(a.l, a.id), C.vb-let, _, a.ann.visit(with-params), "")) { atom-env.env; link(atom-env.atom, atoms) } end new-args = for map2(a from args, at from atoms.reverse()): @@ -1631,7 +1645,7 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com {env; atoms} = for fold(acc from { with-params.env; empty }, a from args): {env; atoms} = acc atom-env = make-atom-for(a.id, a.shadows, env, bindings, - C.value-bind(C.bo-local(a.l, a.id), C.vb-let, _, a.ann.visit(with-params))) + C.value-bind(C.bo-local(a.l, a.id), C.vb-let, _, a.ann.visit(with-params), "")) { atom-env.env; link(atom-env.atom, atoms) } end new-args = for map2(a from args, at from atoms.reverse()): @@ -1721,7 +1735,8 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com new-bind = cases(A.Bind) bind: | s-bind(l2, shadows, name, ann) => atom-env = make-atom-for(name, true, self.env, bindings, - C.value-bind(C.bo-local(l2, name), C.vb-let, _, ann.visit(self))) + # ZACK TODO: maybe improve this? + C.value-bind(C.bo-local(l2, name), C.vb-let, _, ann.visit(self), "VARIANT")) A.s-bind(l2, shadows, atom-env.atom, ann.visit(self)) end A.s-variant-member(l, typ, new-bind) From 7816fbcde02b31fee39f5a6fa54386666456ad00 Mon Sep 17 00:00:00 2001 From: Zack Eisbach Date: Mon, 30 Mar 2026 10:23:40 -0400 Subject: [PATCH 2/6] hover: examples for testing --- vscode/sampleFiles/lsp/hover.arr | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 vscode/sampleFiles/lsp/hover.arr diff --git a/vscode/sampleFiles/lsp/hover.arr b/vscode/sampleFiles/lsp/hover.arr new file mode 100644 index 000000000..3b0d5025d --- /dev/null +++ b/vscode/sampleFiles/lsp/hover.arr @@ -0,0 +1,27 @@ +use context empty-context + +x :: Number +x = 17 + +y :: Number = 38 + +fun not-zero(n :: Number) -> Boolean: + doc: ```answers "is n nonzero?"``` + n <> 0 +end + +div-refine :: Number, Number%(not-zero) -> Number +fun div-refine(num, den): + num / den +end + +fun destruct-some-anns({a; b}, {c :: Number; d :: Number}): + a + b + c + d +end + +fun tup-anns(t :: {Number; Number}, r :: {a :: Number, b :: Number}): + t.{0} + r.a +end + +div2 :: ((n :: Number, m :: Number) -> Boolean) = div-refine +g = lam(n :: Number) -> Boolean: not-zero(n) end From ada52b43ce9fd8f7bb1ca80093de4192ea53a57a Mon Sep 17 00:00:00 2001 From: Zack Eisbach Date: Mon, 30 Mar 2026 11:11:33 -0400 Subject: [PATCH 3/6] hover: piece together annotations --- lang/src/arr/compiler/ast-util.arr | 36 +++++++++++++++++++------ lang/src/arr/compiler/resolve-scope.arr | 17 ++++++------ vscode/sampleFiles/lsp/hover.arr | 1 + 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/lang/src/arr/compiler/ast-util.arr b/lang/src/arr/compiler/ast-util.arr index f31b0cd84..ae6159438 100644 --- a/lang/src/arr/compiler/ast-util.arr +++ b/lang/src/arr/compiler/ast-util.arr @@ -1547,15 +1547,35 @@ fun get-typed-provides(resolved, typed :: TCS.Typed, uri :: URI, compile-env :: end end -fun get-doc-string(expr :: A.Expr) -> String: +fun get-fun-hover-info(expr :: A.Expr) -> {String; A.Ann}: # empty string sounds bad but we already are parsing missing docstrings - # into the empty string, so we have to case anyways... - doc: "extracts the docstring if one exists; empty string otherwise" + # into the empty string, so we have to detect and elide anyways... + # similarly, we detect and elide empty annotations, + # so that is a fine default too. + doc: ``` + Extracts the docstring if one exists; empty string otherwise. + Turns annotations on function-like expressions into arrow annotation; + empty annotation otherwise. + Assumes all tuple annotations have been desugared. + ``` + + fun piece-into-arrow(params :: List, ret :: A.Ann) -> A.Ann%(is-a-arrow-argnames): + a-field-params = for map(p from params): + # s-tuple-binds should be gone by now + a-field(p.l, p.id, p.ann) + end + a-arrow-argnames(A.dummy-loc, a-field-params, ret, false) + end + cases(A.Expr) expr: - | s-lam(_, _, _, _, _, doc, _, _, _, _) => doc - | s-fun(_, _, _, _, _, _, doc, _, _, _, _) => doc - | s-method(_, _, _, _, _, doc, _, _, _, _) => doc - | s-method-field(_, _, _, _, _, doc, _, _, _, _) => doc - | else => "" + | s-lam(_, _, _, params, ann, doc, _, _, _, _) => + {doc; piece-into-arrow(params, ann)} + | s-fun(_, _, _, _, params, ann, doc, _, _, _, _) => + {doc; piece-into-arrow(params, ann)} + | s-method(_, _, _, params, ann, doc, _, _, _, _) => + {doc; piece-into-arrow(params, ann)} + | s-method-field(_, _, _, params, ann, doc, _, _, _, _) => + {doc; piece-into-arrow(params, ann)} + | else => {""; A.a-blank} end end diff --git a/lang/src/arr/compiler/resolve-scope.arr b/lang/src/arr/compiler/resolve-scope.arr index ae5698a07..d2b56218a 100644 --- a/lang/src/arr/compiler/resolve-scope.arr +++ b/lang/src/arr/compiler/resolve-scope.arr @@ -814,14 +814,12 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com # TODO(joe): I think that b.b.ann.visit below could be wrong if # a letrec'd ID is used in a refinement within the same letrec, # so state may be necessary here - + ann = b.b.ann.visit(visitor) + {doc; computed-fun-ann} = U.get-fun-hover-info(b.value) + # only override if there is no annotation written + shadow ann = if A.is-a-blank(ann): computed-fun-ann else: ann end atom-env = make-atom-for(b.b.id, b.b.shadows, env, bindings, - C.value-bind( - C.bo-local(b.l, b.b.id), - C.vb-letrec, - _, - b.b.ann.visit(visitor), - U.get-doc-string(b.value))) + C.value-bind(C.bo-local(b.l, b.b.id), C.vb-letrec, _, ann, doc)) { atom-env.env; link(atom-env.atom, atoms) } end new-visitor = visitor.{env: env} @@ -1502,8 +1500,11 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com cases(A.LetBind) b block: | s-let-bind(l2, bind, expr) => visited-ann = bind.ann.visit(self.{env: e}) + {doc; computed-fun-ann} = U.get-fun-hover-info(b.value) + # only override if there is no annotation written + ann = if A.is-a-blank(ann): computed-fun-ann else: visited-ann end atom-env = make-atom-for(bind.id, bind.shadows, e, bindings, - C.value-bind(C.bo-local(l2, bind.id), C.vb-let, _, visited-ann, U.get-doc-string(expr))) + C.value-bind(C.bo-local(l2, bind.id), C.vb-let, _, ann, doc)) visit-expr = expr.visit(self.{env: e}) new-bind = A.s-let-bind(l2, A.s-bind(l2, bind.shadows, atom-env.atom, visited-ann), visit-expr) { diff --git a/vscode/sampleFiles/lsp/hover.arr b/vscode/sampleFiles/lsp/hover.arr index 3b0d5025d..d9bffd1e8 100644 --- a/vscode/sampleFiles/lsp/hover.arr +++ b/vscode/sampleFiles/lsp/hover.arr @@ -25,3 +25,4 @@ end div2 :: ((n :: Number, m :: Number) -> Boolean) = div-refine g = lam(n :: Number) -> Boolean: not-zero(n) end +g-ann :: Any = lam(n :: Number) -> Boolean: not-zero(n) end From 81a38472ea120279c8ed02b60108dd00833acda2 Mon Sep 17 00:00:00 2001 From: Zack Eisbach Date: Mon, 30 Mar 2026 11:37:50 -0400 Subject: [PATCH 4/6] hover: fix up annotations --- lang/src/arr/compiler/ast-util.arr | 6 +++--- lang/src/arr/compiler/resolve-scope.arr | 2 +- vscode/sampleFiles/lsp/hover.arr | 14 ++++++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/lang/src/arr/compiler/ast-util.arr b/lang/src/arr/compiler/ast-util.arr index ae6159438..aedea21c3 100644 --- a/lang/src/arr/compiler/ast-util.arr +++ b/lang/src/arr/compiler/ast-util.arr @@ -1559,12 +1559,12 @@ fun get-fun-hover-info(expr :: A.Expr) -> {String; A.Ann}: Assumes all tuple annotations have been desugared. ``` - fun piece-into-arrow(params :: List, ret :: A.Ann) -> A.Ann%(is-a-arrow-argnames): + fun piece-into-arrow(params :: List, ret :: A.Ann) -> A.Ann: a-field-params = for map(p from params): # s-tuple-binds should be gone by now - a-field(p.l, p.id, p.ann) + A.a-field(p.l, p.id.tosourcestring(), p.ann) end - a-arrow-argnames(A.dummy-loc, a-field-params, ret, false) + A.a-arrow-argnames(A.dummy-loc, a-field-params, ret, false) end cases(A.Expr) expr: diff --git a/lang/src/arr/compiler/resolve-scope.arr b/lang/src/arr/compiler/resolve-scope.arr index d2b56218a..798e7f1fe 100644 --- a/lang/src/arr/compiler/resolve-scope.arr +++ b/lang/src/arr/compiler/resolve-scope.arr @@ -1502,7 +1502,7 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com visited-ann = bind.ann.visit(self.{env: e}) {doc; computed-fun-ann} = U.get-fun-hover-info(b.value) # only override if there is no annotation written - ann = if A.is-a-blank(ann): computed-fun-ann else: visited-ann end + ann = if A.is-a-blank(visited-ann): computed-fun-ann else: visited-ann end atom-env = make-atom-for(bind.id, bind.shadows, e, bindings, C.value-bind(C.bo-local(l2, bind.id), C.vb-let, _, ann, doc)) visit-expr = expr.visit(self.{env: e}) diff --git a/vscode/sampleFiles/lsp/hover.arr b/vscode/sampleFiles/lsp/hover.arr index d9bffd1e8..0b114e0e1 100644 --- a/vscode/sampleFiles/lsp/hover.arr +++ b/vscode/sampleFiles/lsp/hover.arr @@ -10,19 +10,33 @@ fun not-zero(n :: Number) -> Boolean: n <> 0 end +not-zero + div-refine :: Number, Number%(not-zero) -> Number fun div-refine(num, den): + doc: "divides the things" num / den end +div-refine + fun destruct-some-anns({a; b}, {c :: Number; d :: Number}): a + b + c + d end +destruct-some-anns + fun tup-anns(t :: {Number; Number}, r :: {a :: Number, b :: Number}): t.{0} + r.a end +tup-anns + div2 :: ((n :: Number, m :: Number) -> Boolean) = div-refine +div2 + g = lam(n :: Number) -> Boolean: not-zero(n) end +g + g-ann :: Any = lam(n :: Number) -> Boolean: not-zero(n) end +g-ann From 705fd8ddab62051f5ceb46df3a2e6ecf69137e41 Mon Sep 17 00:00:00 2001 From: Zack Eisbach Date: Sun, 12 Apr 2026 00:16:13 -0400 Subject: [PATCH 5/6] turn unbound type variables into any --- lang/src/arr/compiler/ast-util.arr | 37 +++++++++++++++++++------ lang/src/arr/compiler/resolve-scope.arr | 10 +++---- vscode/sampleFiles/lsp/hover.arr | 8 ++++-- 3 files changed, 39 insertions(+), 16 deletions(-) diff --git a/lang/src/arr/compiler/ast-util.arr b/lang/src/arr/compiler/ast-util.arr index aedea21c3..f304f3145 100644 --- a/lang/src/arr/compiler/ast-util.arr +++ b/lang/src/arr/compiler/ast-util.arr @@ -1547,7 +1547,7 @@ fun get-typed-provides(resolved, typed :: TCS.Typed, uri :: URI, compile-env :: end end -fun get-fun-hover-info(expr :: A.Expr) -> {String; A.Ann}: +fun get-fun-hover-info(expr :: A.Expr, visitor) -> {String; A.Ann}: # empty string sounds bad but we already are parsing missing docstrings # into the empty string, so we have to detect and elide anyways... # similarly, we detect and elide empty annotations, @@ -1567,15 +1567,34 @@ fun get-fun-hover-info(expr :: A.Expr) -> {String; A.Ann}: A.a-arrow-argnames(A.dummy-loc, a-field-params, ret, false) end + # we have to visit the resulting arrow ann to resolve names. + # while doing so, names bound as type parameters in the expr need to be dealt with + # (otherwise they would be unbound). + # The proper thing to do would be to use `a-forall`, which doesn't exist, + # so we use `a-any` as a last resort (at the cost of worse hovering). + fun tparam-visitor(tparams :: List): + tparam-names = tparams.map(_.toname()) + visitor.{ + method a-name(self, l, id): + if A.is-s-name(id) and tparam-names.member(id.s): + # TODO: fix this! + A.a-any(l) + else: + visitor.a-name(l, id) + end + end + } + end + cases(A.Expr) expr: - | s-lam(_, _, _, params, ann, doc, _, _, _, _) => - {doc; piece-into-arrow(params, ann)} - | s-fun(_, _, _, _, params, ann, doc, _, _, _, _) => - {doc; piece-into-arrow(params, ann)} - | s-method(_, _, _, params, ann, doc, _, _, _, _) => - {doc; piece-into-arrow(params, ann)} - | s-method-field(_, _, _, params, ann, doc, _, _, _, _) => - {doc; piece-into-arrow(params, ann)} + | s-lam(_, _, tparams, params, ann, doc, _, _, _, _) => + {doc; piece-into-arrow(params, ann).visit(tparam-visitor(tparams))} + | s-fun(_, _, tparams, _, params, ann, doc, _, _, _, _) => + {doc; piece-into-arrow(params, ann).visit(tparam-visitor(tparams))} + | s-method(_, _, tparams, params, ann, doc, _, _, _, _) => + {doc; piece-into-arrow(params, ann).visit(tparam-visitor(tparams))} + | s-method-field(_, _, tparams, params, ann, doc, _, _, _, _) => + {doc; piece-into-arrow(params, ann).visit(tparam-visitor(tparams))} | else => {""; A.a-blank} end end diff --git a/lang/src/arr/compiler/resolve-scope.arr b/lang/src/arr/compiler/resolve-scope.arr index 798e7f1fe..2ef5b9539 100644 --- a/lang/src/arr/compiler/resolve-scope.arr +++ b/lang/src/arr/compiler/resolve-scope.arr @@ -815,7 +815,7 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com # a letrec'd ID is used in a refinement within the same letrec, # so state may be necessary here ann = b.b.ann.visit(visitor) - {doc; computed-fun-ann} = U.get-fun-hover-info(b.value) + {doc; computed-fun-ann} = U.get-fun-hover-info(b.value, visitor) # only override if there is no annotation written shadow ann = if A.is-a-blank(ann): computed-fun-ann else: ann end atom-env = make-atom-for(b.b.id, b.b.shadows, env, bindings, @@ -1499,14 +1499,14 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com {e; bs; atoms} = acc cases(A.LetBind) b block: | s-let-bind(l2, bind, expr) => - visited-ann = bind.ann.visit(self.{env: e}) - {doc; computed-fun-ann} = U.get-fun-hover-info(b.value) + ann = bind.ann.visit(self.{env: e}) + {doc; computed-fun-ann} = U.get-fun-hover-info(b.value, self.{env: e}) # only override if there is no annotation written - ann = if A.is-a-blank(visited-ann): computed-fun-ann else: visited-ann end + shadow ann = if A.is-a-blank(ann): computed-fun-ann else: ann end atom-env = make-atom-for(bind.id, bind.shadows, e, bindings, C.value-bind(C.bo-local(l2, bind.id), C.vb-let, _, ann, doc)) visit-expr = expr.visit(self.{env: e}) - new-bind = A.s-let-bind(l2, A.s-bind(l2, bind.shadows, atom-env.atom, visited-ann), visit-expr) + new-bind = A.s-let-bind(l2, A.s-bind(l2, bind.shadows, atom-env.atom, ann), visit-expr) { atom-env.env; link(new-bind, bs); diff --git a/vscode/sampleFiles/lsp/hover.arr b/vscode/sampleFiles/lsp/hover.arr index 0b114e0e1..f3b2e1997 100644 --- a/vscode/sampleFiles/lsp/hover.arr +++ b/vscode/sampleFiles/lsp/hover.arr @@ -1,5 +1,3 @@ -use context empty-context - x :: Number x = 17 @@ -40,3 +38,9 @@ g g-ann :: Any = lam(n :: Number) -> Boolean: not-zero(n) end g-ann + +fun generic(l :: List) -> List: + l + l +end + +generic From cc32cdd473d3ad663cb5131971d5ab6ecbdf4bb1 Mon Sep 17 00:00:00 2001 From: Zack Eisbach Date: Sun, 12 Apr 2026 02:08:23 -0400 Subject: [PATCH 6/6] emit doc strings for cross-module hover --- lang/src/arr/compiler/anf-loop-compiler.arr | 3 ++- lang/src/arr/compiler/ast-util.arr | 3 ++- lang/src/arr/compiler/compile-structs.arr | 7 +++--- lang/src/arr/compiler/flatness.arr | 14 +++++++---- lang/src/arr/compiler/resolve-scope.arr | 26 +++++++-------------- lang/src/js/base/type-util.js | 2 ++ vscode/sampleFiles/lsp/hover.arr | 3 +++ 7 files changed, 32 insertions(+), 26 deletions(-) diff --git a/lang/src/arr/compiler/anf-loop-compiler.arr b/lang/src/arr/compiler/anf-loop-compiler.arr index fee2d61f3..52c520ea6 100644 --- a/lang/src/arr/compiler/anf-loop-compiler.arr +++ b/lang/src/arr/compiler/anf-loop-compiler.arr @@ -2179,12 +2179,13 @@ fun compile-provides(provides): j-field("origin", compile-origin(origin)), j-field("typ", compile-provided-type(t)) ])) - | v-fun(origin, t, name, flatness) => + | v-fun(origin, t, name, doc, flatness) => j-field(v, j-obj([clist: j-field("bind", j-str("fun")), j-field("origin", compile-origin(origin)), j-field("flatness", flatness.and-then(j-num).or-else(j-false)), j-field("name", j-str(name)), + j-field("doc", j-str(doc)), j-field("typ", compile-provided-type(t)) ])) end diff --git a/lang/src/arr/compiler/ast-util.arr b/lang/src/arr/compiler/ast-util.arr index f304f3145..2fe7023b0 100644 --- a/lang/src/arr/compiler/ast-util.arr +++ b/lang/src/arr/compiler/ast-util.arr @@ -1363,7 +1363,8 @@ fun canonicalize-value-export(ve :: CS.ValueExport, uri :: URI, tn): | v-alias(o, n) => CS.v-alias(o, n) | v-just-type(o, t) => CS.v-just-type(o, canonicalize-names(t, uri, tn)) | v-var(o, t) => CS.v-var(o, canonicalize-names(t, uri, tn)) - | v-fun(o, t, name, flatness) => CS.v-fun(o, canonicalize-names(t, uri, tn), name, flatness) + | v-fun(o, t, name, doc, flatness) => + CS.v-fun(o, canonicalize-names(t, uri, tn), name, doc, flatness) end end diff --git a/lang/src/arr/compiler/compile-structs.arr b/lang/src/arr/compiler/compile-structs.arr index 30b97fc2f..87aeb555e 100644 --- a/lang/src/arr/compiler/compile-structs.arr +++ b/lang/src/arr/compiler/compile-structs.arr @@ -407,7 +407,7 @@ data ValueExport: | v-alias(origin :: BindOrigin, original-name :: String) | v-just-type(origin :: BindOrigin, t :: T.Type) | v-var(origin :: BindOrigin, t :: T.Type) - | v-fun(origin :: BindOrigin, t :: T.Type, name :: String, flatness :: Option) + | v-fun(origin :: BindOrigin, t :: T.Type, name :: String, doc :: String, flatness :: Option) end data DataExport: @@ -439,7 +439,8 @@ fun value-export-from-raw(uri, val-export, tyvar-env :: SD.StringDict) - t = val-export.tag typ = type-from-raw(uri, val-export.typ, tyvar-env) if t == "v-fun": - v-fun(typ, t, none) + # TODO (ZACK): WTF is up with this? + v-fun(typ, t, none, none) else: v-just-type(typ) end @@ -589,7 +590,7 @@ fun provides-from-raw-provides(uri, raw): else: none end - vdict.set(v.name, v-fun(origin, type-from-raw(uri, v.value.typ, SD.make-string-dict()), v.value.name, flatness)) + vdict.set(v.name, v-fun(origin, type-from-raw(uri, v.value.typ, SD.make-string-dict()), v.value.name, v.value.doc, flatness)) else: origin = origin-from-raw(uri, v.value.origin, v.name) vdict.set(v.name, v-just-type(origin, type-from-raw(uri, v.value.typ, SD.make-string-dict()))) diff --git a/lang/src/arr/compiler/flatness.arr b/lang/src/arr/compiler/flatness.arr index 9ea8a85b0..2bb5e8c95 100644 --- a/lang/src/arr/compiler/flatness.arr +++ b/lang/src/arr/compiler/flatness.arr @@ -349,7 +349,7 @@ fun get-flatness-for-module-fun(id, field, mb, env) -> Flatness: | none => none | some(value-export) => cases(C.ValueExport) value-export: - | v-fun(_, _, _, flatness) => + | v-fun(_, _, _, _, flatness) => flatness | else => none end @@ -452,7 +452,7 @@ fun make-prog-flatness-env(anfed :: AA.AProg, post-env :: C.ComputedEnvironment, | none => nothing | some(ve) => cases(C.ValueExport) ve: - | v-fun(_, _, _, flatness) => sd.set-now(vb.atom.key(), flatness) + | v-fun(_, _, _, _, flatness) => sd.set-now(vb.atom.key(), flatness) | else => nothing end end @@ -462,7 +462,7 @@ fun make-prog-flatness-env(anfed :: AA.AProg, post-env :: C.ComputedEnvironment, raise("The name: " + vb.atom.toname() + " could not be found on the module " + vb.origin.uri-of-definition) | some(value-export) => cases(C.ValueExport) value-export: - | v-fun(_, _, _, flatness) => + | v-fun(_, _, _, _, flatness) => sd.set-now(k, flatness) | else => nothing @@ -568,10 +568,16 @@ fun get-flat-provides(provides, env, post-env, { flatness-env; _ }, ast) block: | v-alias(origin, name) => env.value-by-uri-value(origin.uri-of-definition, origin.original-name.toname()) | else => ve end + existing-doc = cases(C.ValueExport) existing-val: + | v-fun(_, _, _, doc, _) => doc + | else => "" + end cases(Option) maybe-flatness: | none => ve | some(flatness-result) => - C.v-fun(ve.origin, existing-val.t, k, flatness-result) + # only fall back to the existing doc if we have to + doc = if bind.doc == "": existing-doc else: bind.doc end + C.v-fun(ve.origin, existing-val.t, k, doc, flatness-result) end end s.set(k, new-val) diff --git a/lang/src/arr/compiler/resolve-scope.arr b/lang/src/arr/compiler/resolve-scope.arr index 2ef5b9539..e10abc49d 100644 --- a/lang/src/arr/compiler/resolve-scope.arr +++ b/lang/src/arr/compiler/resolve-scope.arr @@ -767,18 +767,12 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com cases(Option) val-info block: | none => raise("The value is a global that doesn't exist in any module: " + name) | some(shadow val-info) => - cases(C.ValueExport) val-info block: - | v-var(_, t) => - # ZACK TODO: what should we do with vars here?? - b = C.value-bind(C.bo-global(some(origin), uri-of-definition, origin.original-name), C.vb-var, names.s-global(A.dummy-loc, name), A.a-blank, "") - bindings.set-now(names.s-global(A.dummy-loc, name).key(), b) - acc.set-now(name, b) - | else => - # TODO(joe): Good place to add _location_ to valueexport to report errs better - b = C.value-bind(C.bo-global(some(origin), uri-of-definition, origin.original-name), C.vb-let, names.s-global(A.dummy-loc, name), A.a-blank, "IMPORT PLACEHOLDER") - bindings.set-now(names.s-global(A.dummy-loc, name).key(), b) - acc.set-now(name, b) - end + vbinder = if C.is-v-var(val-info): C.vb-var else: C.vb-let end + doc = if C.is-v-fun(val-info): val-info.doc else: "" end + b = C.value-bind(C.bo-global(some(origin), uri-of-definition, origin.original-name), + vbinder, names.s-global(A.dummy-loc, name), A.a-blank, doc) + bindings.set-now(names.s-global(A.dummy-loc, name).key(), b) + acc.set-now(name, b) end end acc.freeze() @@ -891,16 +885,14 @@ fun resolve-names(p :: A.Program, thismodule-uri :: String, initial-env :: C.Com name-errors := link(C.name-not-provided(l, imp-loc, vname, "value"), name-errors) env | some(value-export) => - vbinder = cases(C.ValueExport) value-export block: - | v-var(_, t) => C.vb-var - | else => C.vb-let - end + vbinder = if C.is-v-var(value-export): C.vb-var else: C.vb-let end + doc = if C.is-v-fun(value-export): value-export.doc else: "" end atom-env = make-import-atom-for(as-name, value-export.origin.uri-of-definition, env, bindings, C.value-bind(C.bo-module(as-name.l, value-export.origin.definition-bind-site, value-export.origin.uri-of-definition, value-export.origin.original-name), - vbinder, _, A.a-any(vname.l), "VALUE ENV PLACEHOLDER")) + vbinder, _, A.a-any(vname.l), doc)) atom-env.env end end diff --git a/lang/src/js/base/type-util.js b/lang/src/js/base/type-util.js index e070ca43e..743a05dc2 100644 --- a/lang/src/js/base/type-util.js +++ b/lang/src/js/base/type-util.js @@ -157,6 +157,7 @@ define("pyret-base/js/type-util", [], function() { origin: origin, bind: "fun", name: value.name || "", + doc: value.doc || "", flatness: flatness, typ: toPyretType(runtime, expandType(value.typ, shorthands)) }); @@ -287,6 +288,7 @@ define("pyret-base/js/type-util", [], function() { origin: typ.origin, flatness: typ.flatness, name: typ.name, + doc: typ.doc || "", typ: expandType(typ.typ, shorthands) }; } diff --git a/vscode/sampleFiles/lsp/hover.arr b/vscode/sampleFiles/lsp/hover.arr index f3b2e1997..26c33f530 100644 --- a/vscode/sampleFiles/lsp/hover.arr +++ b/vscode/sampleFiles/lsp/hover.arr @@ -44,3 +44,6 @@ fun generic(l :: List) -> List: end generic + +map +