Repository: usethesource/typepal
Pull request #14: Rename framework
Author: login: toinehartman
Labels: name: enhancement
Created: 2025-05-01T15:27:18Z | Merged: 2025-05-19T08:34:12Z

Port rename framework from https://github.com/SWAT-engineering/rascal-rename-framework/


Diff:
diff --git a/src/analysis/typepal/refactor/Rename.rsc b/src/analysis/typepal/refactor/Rename.rsc
new file mode 100644
index 00000000..76de4bae
--- /dev/null
+++ b/src/analysis/typepal/refactor/Rename.rsc
@@ -0,0 +1,411 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module analysis::typepal::refactor::Rename
+
+import analysis::diff::edits::TextEdits;
+import analysis::typepal::FailMessage;
+import analysis::typepal::Messenger;
+import analysis::typepal::TModel;
+
+import util::Monitor;
+
+import Exception;
+import IO;
+import List;
+import Map;
+import Message;
+import Node;
+import ParseTree;
+import Relation;
+import Set;
+
+alias RenameResult = tuple[list[DocumentEdit], set[Message]];
+alias Focus = list[Tree];
+
+// This leaves some room for more fine-grained steps should the user want to monitor that
+private int WORKSPACE_WORK = 10;
+private int FILE_WORK = 5;
+
+data Renamer
+    = renamer(
+        void(FailMessage) msg
+      , void(DocumentEdit) documentEdit
+      , void(TextEdit) textEdit
+      , RenameConfig() getConfig
+    );
+
+@synopsis{Language-specific configuration of identifier renaming.}
+@description{
+Configures the rename for a specific language, by giving the following mandatory arguments:
+* `Tree parseLoc(loc l)`, which parses a file and returns a ((ParseTree::Tree)). Typically this would be `parse(#StartSymbol, l)`.
+* `TModel tmodelForTree(Tree t)`, which type-checks a ((ParseTree::Tree)) and returns a ((TModel)). Typically, this would be ((collectAndSolve)).
+The configuration also takes some optional arguments:
+* `TModel tmodelForLoc(loc l)`, which type-checks a file given its `loc` instead of its parse tree. This can be useful when the file does not have to be parsed to produce a ((TModel)), for example when it had already been type-checked just before starting the renaming. Defaults to `tmodelForTree(parseLoc(l))`.
+* `bool debug`, which indicates whether debug output should be printed during the renaming. Defaults to `false`.
+*`str jobLabel`, which can be used to change the base label of the rename progress bar. Defaults to `"Renaming"`.
+}
+data RenameConfig
+    = rconfig(
+        Tree(loc) parseLoc
+      , TModel(Tree) tmodelForTree
+      , TModel(loc) tmodelForLoc = TModel(loc l) { return tmodelForTree(parseLoc(l)); }
+      , bool debug = false
+      , str jobLabel = "Renaming"
+    );
+
+@synopsis{Sorts ((analysis::diff::edits::TextEdits::DocumentEdit))s.}
+@description{
+Applying edits through ((analysis::diff::edits::ExecuteTextEdits)) should happen in a specific order.
+Specifically, files should be created before they can be modified, and after renaming them, modifications/deletions should refer to the new name.
+This functions sorts edits in the following order.
+
+1. ((analysis::diff::edits::TextEdits::created))
+2. ((analysis::diff::edits::TextEdits::changed))
+3. ((analysis::diff::edits::TextEdits::renamed))
+4. ((analysis::diff::edits::TextEdits::removed))
+}
+list[DocumentEdit] sortDocEdits(list[DocumentEdit] edits) = sort(edits, bool(DocumentEdit e1, DocumentEdit e2) {
+    if (e1 is created && !(e2 is created)) return true;
+    if (e1 is changed && !(e2 is changed)) return !(e2 is created);
+    if (e1 is renamed && !(e2 is renamed)) return (e2 is removed);
+    return false;
+});
+
+@synopsis{Renames the identifier under the cursor to `newName` and returns the required ((analysis::diff::edits::TextEdits::DocumentEdit))s and ((Message::Message))s.}
+@description{
+Renames the identifier under the cursor (represented as a ((Focus))) to `newName`, given a specific ((analysis::typepal::refactor::Rename::RenameConfig)).
+This renaming uses ((TModel))s produced by the type-checker.
+
+The rename framework provides a default implementation, which can be selectively extended for languages that require different rename behaviour than the default. These functions constitute the implementation of renaming:
+* ((analysis::typepal::refactor::Rename::getCursorDefinitions))
+* ((analysis::typepal::refactor::Rename::findOccurrenceFiles))
+* ((analysis::typepal::refactor::Rename::findAdditionalDefinitions))
+* ((analysis::typepal::refactor::Rename::validateNewNameOccurrences))
+* ((analysis::typepal::refactor::Rename::renameDefinition))
+* ((analysis::typepal::refactor::Rename::renameUses))
+* ((analysis::typepal::refactor::Rename::nameLocation))
+}
+@pitfalls{
+* Since the ((analysis::typepal::refactor::Rename::RenameConfig)) that this function passes to the various hooks contains state and caches, it should never escape the scope of a single invoation of ((rename)). Instead, it should always be accessed via ((renamer))'s `getConfig`.
+* The renaming highly depends on a type-check function `TModel(Tree)`. If such a function does not exist, this framework cannot function.
+}
+RenameResult rename(
+        Focus cursor
+      , str newName
+      , RenameConfig config) {
+
+    jobStart(config.jobLabel, totalWork = 2 * WORKSPACE_WORK + 1);
+    jobStep(config.jobLabel, "Initializing renaming");
+
+    // Tree & TModel caching
+
+    @memo{maximumSize(50)}
+    TModel getTModelCached(Tree t) {
+        tm = config.tmodelForTree(t);
+        if (msg <- tm.messages, msg is error) registerMessage(error(t.src.top, "Renaming failed, since this file has type error(s)."));
+        return tm;
+    }
+
+    @memo{maximumSize(50)}
+    TModel getTModelForLocCached(loc l) = config.tmodelForLoc(l);
+
+    @memo{maximumSize(50)}
+    Tree parseLocCached(loc l) {
+        // We already have the parse tree of the module under cursor
+        if (l == cursor[-1].src.top) {
+            return cursor[-1];
+        }
+
+        try {
+            return config.parseLoc(l);
+        } catch ParseError(_): {
+            registerMessage(error(l, "Renaming failed, since an error occurred while parsing this file."));
+            return char(-1);
+        }
+    }
+
+    // Make sure user uses cached functions
+    cachedConfig = config
+        [parseLoc = parseLocCached]
+        [tmodelForTree = getTModelCached]
+        [tmodelForLoc = getTModelForLocCached]
+        ;
+
+    // Messages
+    set[FailMessage] messages = {};
+    bool errorReported() = messages != {} && any(m <- messages, m is fm_error);
+    void registerMessage(FailMessage msg) { messages += msg; };
+    AType getType(Tree t) {
+        TModel tm = getTModelCached(parseLocCached(t.src.top));
+        if (tm.facts[t.src]?) {
+            return tm.facts[t.src];
+        }
+
+        return tvar(|unknown:///|);
+    }
+    set[Message] getMessages() = {toMessage(m, getType) | m <- messages};
+
+    // Edits
+    set[loc] editsSeen = {};
+    list[DocumentEdit] docEdits = [];
+
+    void checkEdit(replace(loc range, _)) {
+        if (range in editsSeen) {
+            registerMessage(error(range, "Multiple replace edits for this location."));
+        }
+        editsSeen += range;
+    }
+
+    void checkEdit(DocumentEdit e) {
+        loc file = e has file ? e.file : e.from;
+        if (changed(f, tes) := e) {
+            // Check contents of DocumentEdit
+            for (te:replace(range, _) <- tes) {
+                // Check integrity
+                if (range.top != f) {
+                    registerMessage(error(range, "Invalid replace edit for this location. This location is not in <f>, for which it was registered."));
+                }
+
+                // Check text edits
+                checkEdit(te);
+            }
+        } else if (file in editsSeen) {
+            registerMessage(error(file, "Multiple <getName(e)> edits for this file."));
+        }
+
+        editsSeen += file;
+    }
+
+    void registerDocumentEdit(DocumentEdit e) {
+        checkEdit(e);
+        docEdits += e;
+    };
+
+    void registerTextEdit(TextEdit e) {
+        checkEdit(e);
+
+        loc f = e.range.top;
+        if ([*pre, c:changed(f, _)] := docEdits) {
+            // If possible, merge with latest document edit
+            // TODO Just assign to docEdits[-1], once this issue has been solved:
+            // https://github.com/usethesource/rascal/issues/2123
+            docEdits = [*pre, c[edits = c.edits + e]];
+        } else {
+            // Else, create new document edit
+            docEdits += changed(f, [e]);
+        }
+    };
+
+    Renamer r = renamer(
+        registerMessage
+      , registerDocumentEdit
+      , registerTextEdit
+      , RenameConfig() { return cachedConfig; }
+    );
+
+    jobStep(config.jobLabel, "Resolving definitions of <cursor[0].src>", work = WORKSPACE_WORK);
+    defs = getCursorDefinitions(cursor, parseLocCached, getTModelCached, r);
+
+    if (defs == {}) r.msg(error(cursor[0].src, "No definitions found"));
+    if (errorReported()) {
+        jobEnd(config.jobLabel, success=false);
+        return <sortDocEdits(docEdits), getMessages()>;
+    }
+
+    jobStep(config.jobLabel, "Looking for files with occurrences of name under cursor", work = WORKSPACE_WORK);
+    <maybeDefFiles, maybeUseFiles, newNameFiles> = findOccurrenceFiles(defs, cursor, newName, parseLocCached, r);
+
+    jobTodo(config.jobLabel, work = (size(maybeDefFiles) + size(maybeUseFiles) + size(newNameFiles)) * FILE_WORK);
+
+    set[Define] additionalDefs = {};
+    solve (additionalDefs) {
+        for (loc f <- maybeDefFiles) {
+            jobStep(config.jobLabel, "Looking for additional definitions in <f>", work = 0);
+            tr = parseLocCached(f);
+            tm = getTModelCached(tr);
+            additionalDefs += findAdditionalDefinitions(defs, tr, tm, r);
+        }
+        defs += additionalDefs;
+    }
+    jobStep(config.jobLabel, "Done looking for additional definitions", work = FILE_WORK * size(maybeDefFiles));
+
+    for (loc f <- newNameFiles) {
+        jobStep(config.jobLabel, "Validating occurrences of new name \'<newName>\' in <f>", work = FILE_WORK);
+        tr = parseLocCached(f);
+        validateNewNameOccurrences(defs, newName, tr, r);
+    }
+    if (errorReported()) {
+        jobEnd(config.jobLabel, success = false);
+        return <sortDocEdits(docEdits), getMessages()>;
+    }
+
+    defFiles = {d.defined.top | d <- defs};
+    jobTodo(config.jobLabel, work = size(defFiles) * FILE_WORK);
+
+    for (loc f <- defFiles) {
+        fileDefs = {d | d <- defs, d.defined.top == f};
+        jobStep(config.jobLabel, "Renaming <size(fileDefs)> definitions in <f>", work = FILE_WORK);
+        tr = parseLocCached(f);
+        tm = getTModelCached(tr);
+
+        map[Define, loc] defNames = defNameLocations(tr, fileDefs, r);
+        for (d <- fileDefs) {
+            renameDefinition(d, defNames[d] ? d.defined, newName, tm, r);
+        }
+    }
+
+    if (errorReported()) {
+        jobEnd(config.jobLabel, success=false);
+        return <sortDocEdits(docEdits), getMessages()>;
+    }
+
+    for (loc f <- maybeUseFiles) {
+        jobStep(config.jobLabel, "Renaming uses in <f>", work = FILE_WORK);
+        tr = parseLocCached(f);
+        tm = getTModelCached(tr);
+
+        renameUses(defs, newName, tm, r);
+    }
+
+    set[Message] convertedMessages = getMessages();
+
+    if (config.debug) {
+        println("\n\n=================\nRename statistics\n=================\n");
+        int nDocs = size({f | de <- docEdits, f := (de has file ? de.file : de.from)});
+        int nEdits = (0 | it + ((changed(_, tes) := e) ? size(tes) : 1) | e <- docEdits);
+
+        int nErrors = size({msg | msg <- convertedMessages, msg is error});
+        int nWarnings = size({msg | msg <- convertedMessages, msg is warning});
+        int nInfos = size({msg | msg <- convertedMessages, msg is info});
+
+        println(" # of documents affected: <nDocs>");
+        println(" # of text edits:         <nEdits>");
+        println(" # of messages:           <size(convertedMessages)>");
+        println("   (<nErrors> errors, <nWarnings> warnings and <nInfos> infos)");
+
+        if (size(convertedMessages) > 0) {
+            println("\n===============\nMessages\n===============");
+            for (msg <- convertedMessages) {
+                println(" ** <msg>");
+            }
+            println();
+        }
+    }
+
+    jobEnd(config.jobLabel, success = !errorReported());
+    return <sortDocEdits(docEdits), convertedMessages>;
+}
+
+// TODO If performance bottleneck, rewrite to binary search
+private map[Define, loc] defNameLocations(Tree tr, set[Define] defs, Renamer _r) {
+    map[loc, Define] definitions = (d.defined: d | d <- defs);
+    set[loc] defsToDo = defs.defined;
+
+    // If we have a single definition, we can put the pattern matcher to work
+    if ({loc d} := defsToDo) {
+        def = definitions[d];
+        top-down visit (tr) {
+            case t:appl(_, _):
+                if (t@\loc?, d := t@\loc) {
+                    return (def: nameLocation(t, def));
+                }
+        }
+    }
+
+    map[Define, loc] defNames = ();
+    for (defsToDo != {}, /t:appl(_, _) := tr, t@\loc?, loc d := t@\loc, d in defsToDo) {
+        def = definitions[d];
+        defNames[def] = nameLocation(t, def);
+        defsToDo -= d;
+    }
+
+    return defNames;
+}
+
+@synopsis{Computes ((Define))(s) for the name under the cursor.}
+default set[Define] getCursorDefinitions(Focus cursor, Tree(loc) _r, TModel(Tree) getModel, Renamer r) {
+    loc cursorLoc = cursor[0].src;
+    TModel tm = getModel(cursor[-1]);
+    for (Tree c <- cursor) {
+        if (tm.definitions[c.src]?) {
+            return {tm.definitions[c.src]};
+        } else if (defs: {_, *_} := tm.useDef[c.src]) {
+            if (any(d <- defs, d.top != cursorLoc.top)) {
+                r.msg(error(cursorLoc, "Rename not implemented for cross-file definitions. Please overload `getCursorDefinitions`."));
+                return {};
+            }
+
+            return {tm.definitions[d] | d <- defs, tm.definitions[d]?};
+        }
+    }
+
+    r.msg(error(cursorLoc, "Could not find definition to rename."));
+    return {};
+}
+
+@synopsis{Computes in which files occurrences of `cursorDefs` and `newName` *might* occur (over-approximation). This is not supposed to call the type-checker on any file for performance reasons.}
+default tuple[set[loc] defFiles, set[loc] useFiles, set[loc] newNameFiles] findOccurrenceFiles(set[Define] cursorDefs, Focus cursor, str newName, Tree(loc) _getTree, Renamer r) {
+    loc f = cursor[0].src.top;
+    if (any(d <- cursorDefs, f != d.defined.top)) {
+        r.msg(error(cursor[0].src, "Rename not implemented for cross-file definitions. Please overload `findOccurrenceFiles`."));
+        return <{}, {}, {}>;
+    }
+
+    return <{f}, {f}, any(/Tree t := f, "<t>" == newName) ? {f} : {}>;
+}
+
+@synopsis{Computes additional definitions (e.g. overloads of `cursorDefs`) in a single file.}
+default set[Define] findAdditionalDefinitions(set[Define] _cursorDefs, Tree _tr, TModel _tm, Renamer _r) = {};
+
+@synopsis{Validates for a single file with occurrences of `newName` that, when renaming all occurrences of `cursorDefs` to `newName`, no problems will be introduced.}
+@examples{This could be used to detect many kinds of problems, e.g. static errors and semantic changes due to shadowing or overloading.}
+default void validateNewNameOccurrences(set[Define] cursorDefs, str newName, Tree tr, Renamer r) {
+    for (Define d <- cursorDefs) {
+        r.msg(error(d.defined, "Renaming this to \'<newName>\' would clash with use of \'<newName>\' in <tr.src.top>."));
+    }
+}
+
+@synopsis{Renames a single ((Define)) with its name at `nameLoc`, by producing a corresponding ((DocumentEdit)).}
+default void renameDefinition(Define _d, loc nameLoc, str newName, TModel _tm, Renamer r) {
+    r.textEdit(replace(nameLoc, newName));
+}
+
+@synopsis{{Renames all uses of `defs` in a single file/((TModel)), by producing corresponding ((DocumentEdit))s.}}
+default void renameUses(set[Define] defs, str newName, TModel tm, Renamer r) {
+    for (loc u <- invert(tm.useDef)[defs.defined] - defs.defined) {
+        r.textEdit(replace(u, newName));
+    }
+}
+
+@synopsis{Finds the location of the identifier within definition ((ParseTree::Tree)) `t` corresponding to ((Define)) `d`, where `t.src == d.defined`.}
+default loc nameLocation(Tree t, Define d) {
+    // Try to find the first sub-tree that matches the name of the definition
+    for (/Tree tr := t, tr@\loc?, "<tr>" == d.id) {
+        return tr@\loc;
+    }
+    return t@\loc;
+}
diff --git a/src/examples/fun/Checker.rsc b/src/examples/fun/Checker.rsc
index 8268928d..a3b7376b 100644
--- a/src/examples/fun/Checker.rsc
+++ b/src/examples/fun/Checker.rsc
@@ -88,11 +88,15 @@ void collect(current: (Expression) `<Id name>`,  Collector c){
 void collect(current: (Expression) `<Expression exp1> (<Expression exp2>)`, Collector c) { 
      c.calculate("application", current, [exp1, exp2],
          AType (Solver s) {  
-            if(functionType(tau1, tau2) := s.getType(exp1)){
+            tp = s.getType(exp1);
+            if(functionType(tau1, tau2) := tp){
+                  s.requireEqual(exp2, tau1, error(exp2, "Incorrect type of actual parameter"));
+                  return tau2;
+               } else if (overloadedAType(types) := tp, {<_, functionType(tau1, tau2)>} := types<1, 2>) {
                   s.requireEqual(exp2, tau1, error(exp2, "Incorrect type of actual parameter"));
                   return tau2;
                } else {
-                  s.report(error(exp1, "Function type expected"));
+                  s.report(error(exp1, "Function type expected, got %t", s.getType(exp1)));
                   return intType();
                }
         });
diff --git a/src/examples/modfun/A.mfun b/src/examples/modfun/A.mfun
new file mode 100644
index 00000000..144382ad
--- /dev/null
+++ b/src/examples/modfun/A.mfun
@@ -0,0 +1,5 @@
+module A {
+    import B;
+    def a : int -> int = fun i : int { 5 + i };
+    def a : int -> int = fun i : int { 6 + i };
+}
diff --git a/src/examples/modfun/B.mfun b/src/examples/modfun/B.mfun
new file mode 100644
index 00000000..de31453a
--- /dev/null
+++ b/src/examples/modfun/B.mfun
@@ -0,0 +1,4 @@
+module B {
+    import A;
+    def j : int = a(1);
+}
\ No newline at end of file
diff --git a/src/examples/modfun/Checker.rsc b/src/examples/modfun/Checker.rsc
index 613d0c1e..6daef903 100644
--- a/src/examples/modfun/Checker.rsc
+++ b/src/examples/modfun/Checker.rsc
@@ -17,6 +17,11 @@ module examples::modfun::Checker
 import examples::modfun::Syntax;
 extend examples::fun::Checker;
 
+import IO;
+import String;
+
+private str MODULES_IMPORT_QUEUE = "__modulesImportQueue";
+
 // ----  IdRoles, PathLabels and AType ---------------------------------------- 
      
 data AType
@@ -43,6 +48,7 @@ void collect(current: (ModuleDecl) `module <ModId mid> { <Decl* decls> }`, Colle
 
 void collect(current: (ImportDecl) `import <ModId mid> ;`, Collector c){
      c.addPathToDef(mid, {moduleId()}, importPath());
+     c.push(MODULES_IMPORT_QUEUE, "<mid>");
 }
 
 void collect(current: (VarDecl) `def <Id id> : <Type tp> = <Expression expression> ;`, Collector c)     {
@@ -50,3 +56,52 @@ void collect(current: (VarDecl) `def <Id id> : <Type tp> = <Expression expressio
      c.requireEqual(tp, expression, error(current, "Expected initializing expression of type %t, found %t", expression, tp));
      collect(tp, expression, c);
 }
+
+TModel modfunTModelFromTree(Tree pt, TypePalConfig config) {
+    if (pt has top) pt = pt.top;
+    c = newCollector("modfun", pt, config);
+    collect(pt, c);
+    handleImports(c, pt, pathConfig(pt@\loc));
+    return newSolver(pt, c.run()).run();
+}
+
+void handleImports(Collector c, Tree root, PathConfig pcfg) {
+    set[str] imported = {};
+    while (list[str] modulesToImport := c.getStack(MODULES_IMPORT_QUEUE) && modulesToImport != []) {
+        c.clearStack(MODULES_IMPORT_QUEUE);
+        for (m <- modulesToImport, m notin imported) {
+            if (<true, l> := lookupModule(m, pcfg)) {
+                collect(parse(#start[ModFun], l).top, c);
+            }
+            else {
+                c.report(error(root, "Cannot find module %v in %v or %v", m, pcfg.srcs, pcfg.libs));
+            }
+            imported += m;
+        }
+    }
+}
+
+private loc project(loc file) {
+   assert file.scheme == "project";
+   return |project://<file.authority>|;
+}
+
+data PathConfig = pathConfig(list[loc] srcs = [], list[loc] libs = []);
+
+PathConfig pathConfig(loc file) {
+   assert file.scheme == "project";
+
+   p = project(file);
+
+   return pathConfig(srcs = [ p + "src/examples/modfun"]);
+}
+
+tuple[bool, loc] lookupModule(str name, PathConfig pcfg) {
+    for (s <- pcfg.srcs + pcfg.libs) {
+        result = (s + replaceAll(name, "::", "/"))[extension = "mfun"];
+        if (exists(result)) {
+            return <true, result>;
+        }
+    }
+    return <false, |invalid:///|>;
+}
diff --git a/src/examples/modfun/Rename.rsc b/src/examples/modfun/Rename.rsc
new file mode 100644
index 00000000..c92c72fa
--- /dev/null
+++ b/src/examples/modfun/Rename.rsc
@@ -0,0 +1,156 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+
+module examples::modfun::Rename
+
+import examples::modfun::Checker;
+import examples::modfun::Syntax;
+
+extend analysis::typepal::refactor::Rename;
+import analysis::diff::edits::TextEdits;
+
+import Exception;
+import IO;
+import List;
+import Map;
+import Relation;
+import util::FileSystem;
+import util::Maybe;
+
+data RenameConfig(
+    set[loc] srcs = {}
+);
+
+public tuple[list[DocumentEdit] edits, set[Message] msgs] renameModules(list[Tree] cursor, str newName) {
+    bool nameIsValid = any(ModId _ <- cursor)
+        ? isValidName(moduleId(), newName)
+        : isValidName(variableId(), newName);
+
+    if (!nameIsValid) {
+        return <[], {error("Invalid name: <newName>", cursor[0].src)}>;
+    }
+
+    return rename(
+        cursor
+      , newName
+      , rconfig(
+          Tree(loc l) { return parse(#start[ModFun], l); }
+        , TModel(Tree pt) { return modfunTModelFromTree(pt, tconfig(mayOverload = bool(set[loc] defs, map[loc, Define] defines) { return toRel(defines)[defs].idRole == {variableId()}; })); }
+        , srcs = {cursor[0].src.top.parent}
+        , jobLabel = "Renaming \'<cursor[0]>\' to \'<newName>\' at <cursor[0].src>"
+      )
+    );
+}
+
+private Maybe[tuple[IdRole, str]] analyzeUse((ImportDecl) `import <ModId id>;`) = just(<moduleId(), "<id>">);
+private Maybe[tuple[IdRole, str]] analyzeUse((Expression) `<Id id>`) = just(<variableId(), "<id>">);
+private default Maybe[tuple[IdRole, str]] analyzeUse(Tree _) = nothing();
+
+private Maybe[tuple[IdRole, str]] analyzeDef((ModuleDecl) `module <ModId id> { <Decl* _> }`) = just(<moduleId(), "<id>">);
+private Maybe[tuple[IdRole, str]] analyzeDef((VarDecl) `def <Id id> : <Type _> = <Expression _>;`) = just(<variableId(), "<id>">);
+private default Maybe[tuple[IdRole, str]] analyzeDef(Tree _) = nothing();
+
+set[Define] getCursorDefinitions(list[Tree] cursor, Tree(loc) getTree, TModel(Tree) getModel, Renamer r) {
+    TModel tm = getModel(cursor[-1]);
+    for (Tree c <- cursor) {
+        if (tm.definitions[c.src]?) {
+            return {tm.definitions[c.src]};
+        } else {
+            str cursorName = "<c>";
+            set[Define] defs = {};
+            for (loc f <- (tm.paths<pathRole, to>)[importPath()]) {
+                Tree fTree = getTree(f.top);
+                for (/Tree t := fTree, just(<idRole, cursorName>) := analyzeDef(t)) {
+                    tm = getModel(fTree);
+                    defs += {d | Define d:<_, cursorName, _, idRole, _, _> <- tm.defines};
+                }
+            }
+            if (defs != {}) return defs;
+        }
+    }
+
+    r.msg(error(cursor[0].src, "Could not find definition to rename."));
+    return {};
+}
+
+tuple[set[loc], set[loc], set[loc]] findOccurrenceFiles(set[Define] defs, list[Tree] cursor, str newName, Tree(loc) getTree, Renamer r) {
+    set[loc] defFiles = {};
+    set[loc] useFiles = {};
+    set[loc] newNameFiles = {};
+
+    for (Define _:<_, name, _, idRole, _, _> <- defs) {
+        for (loc srcFolder <- r.getConfig().srcs
+           , loc f <- find(srcFolder, "mfun")) {
+            for (/Tree t := getTree(f)) {
+                if (just(<idRole, str n>) := analyzeDef(t)) {
+                    if (n == name) defFiles += f;
+                    else if (n == newName) newNameFiles += f;
+                }
+                if (just(<idRole, str n>) := analyzeUse(t)) {
+                    if (n == name) useFiles += f;
+                    else if (n == newName) newNameFiles += f;
+                }
+            }
+        }
+    }
+
+    return <defFiles, useFiles, newNameFiles>;
+}
+
+bool tryParse(type[&T <: Tree] tp, str s) {
+    try {
+        parse(tp, s);
+        return true;
+    } catch ParseError(_): {
+        return false;
+    }
+}
+
+bool isValidName(moduleId(), str name) = tryParse(#ModId, name);
+bool isValidName(variableId(), str name) = tryParse(#Id, name);
+
+set[Define] findAdditionalDefinitions(set[Define] cursorDefs, Tree _, TModel tm, Renamer _) {
+    set[Define] overloads = {};
+    for (d <- tm.defines
+      && d.idRole in cursorDefs.idRole
+      && d.id in cursorDefs.id
+      && d.defined notin cursorDefs.defined) {
+        if (tm.config.mayOverload(cursorDefs.defined + d.defined, tm.definitions)) {
+            overloads += d;
+        }
+    }
+    return overloads;
+}
+
+void renameUses(set[Define] defs, str newName, TModel tm, Renamer r) {
+    // Somehow, tm.useDef is empty, so we need to use tm.uses
+    rel[loc, loc] defUse = {<d, u> | <Define _:<_, id, orgId, idRole, d, _>, use(id, orgId, u, _, _)> <- defs * toSet(tm.uses)};
+
+    for (loc u <- defUse[defs.defined]) {
+        r.textEdit(replace(u, newName));
+    }
+}
diff --git a/src/examples/modfun/RenameTest.rsc b/src/examples/modfun/RenameTest.rsc
new file mode 100644
index 00000000..c0a35432
--- /dev/null
+++ b/src/examples/modfun/RenameTest.rsc
@@ -0,0 +1,84 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module examples::modfun::RenameTest
+
+import examples::modfun::Rename;
+import examples::modfun::Syntax;
+
+import analysis::diff::edits::TextEdits;
+
+import examples::rename::Focus;
+import IO;
+import List;
+import ParseTree;
+import Set;
+import String;
+import util::FileSystem;
+
+tuple[list[DocumentEdit] edits, set[Message] msgs] basicRename(str modName, int line, int col, str newName = "foo") {
+    prog = parse(#start[ModFun], |project://typepal/src/examples/modfun/<modName>.mfun|);
+    cursor = computeFocusList(prog, line, col);
+    return renameModules(cursor, newName);
+}
+
+list[int] sortedEditAmounts(list[DocumentEdit] edits) =
+    sort([size(e.edits) | e <- edits]);
+
+void checkNoErrors(set[Message] msgs) {
+    if (m <- msgs, m is error) {
+        throw "Renaming threw errors:\n - <intercalate("\n - ", toList(msgs))>";
+    }
+}
+
+test bool duplicateModuleName() {
+    <edits, msgs> = basicRename("A", 1, 8, newName = "B");
+    return size({m | m <- msgs, m is error}) >= 1;
+}
+
+test bool moduleName() {
+    <edits, msgs> = basicRename("A", 1, 8, newName = "C");
+
+    checkNoErrors(msgs);
+    return size(edits) == 2
+        && sortedEditAmounts(edits) == [1, 1];
+}
+
+test bool overloadedFunc() {
+    <edits, msgs> = basicRename("A", 3, 9);
+
+    checkNoErrors(msgs);
+    return size(edits) == 2
+        && sortedEditAmounts(edits) == [1, 2];
+}
+
+test bool importedFunc() {
+    <edits, msgs> = basicRename("B", 3, 19);
+
+    checkNoErrors(msgs);
+    return size(edits) == 2
+        && sortedEditAmounts(edits) == [1, 2];
+}
diff --git a/src/examples/modfun/tmp.mfun b/src/examples/modfun/tmp.mfun
index a44f858e..e3e3c188 100644
--- a/src/examples/modfun/tmp.mfun
+++ b/src/examples/modfun/tmp.mfun
@@ -1 +1 @@
-module A { def x : int = y; }
\ No newline at end of file
+module A { def x : int = 1; }
\ No newline at end of file
diff --git a/src/examples/modules/C.modules b/src/examples/modules/C.modules
index 89d2718e..8b58a7a2 100644
--- a/src/examples/modules/C.modules
+++ b/src/examples/modules/C.modules
@@ -4,6 +4,6 @@ import A
 import B
 
 struct C1 {
-	A1
-	B1
+    A1
+    B1
 }
\ No newline at end of file
diff --git a/src/examples/modules/Checker.rsc b/src/examples/modules/Checker.rsc
index 23c6e2a2..b2250033 100644
--- a/src/examples/modules/Checker.rsc
+++ b/src/examples/modules/Checker.rsc
@@ -49,17 +49,16 @@ PathConfig pathConfig(loc file) {
 
    p = project(file);      
  
-   return pathConfig(srcs = [ p + "src/lang/modules"]);
+   return pathConfig(srcs = [ p + "src/examples/modules"]);
 }
 
-private str __MODULES_IMPORT_QUEUE = "__modulesImportQueue";
+private str MODULES_IMPORT_QUEUE = "__modulesImportQueue";
 
 str getFileName((ModuleId) `<{Id "::"}+ moduleName>`) = replaceAll("<moduleName>.modules", "::", "/");
 
 tuple[bool, loc] lookupModule(str name, PathConfig pcfg) {
     for (s <- pcfg.srcs + pcfg.libs) {
         result = (s + replaceAll(name, "::", "/"))[extension = "modules"];
-        println(result);
         if (exists(result)) {
         	return <true, result>;
         }
@@ -69,13 +68,13 @@ tuple[bool, loc] lookupModule(str name, PathConfig pcfg) {
 
 void collect(current:(Import) `import <ModuleId moduleName>`, Collector c) {
     c.addPathToDef(moduleName, {moduleId()}, importPath());
-    c.push(__MODULES_IMPORT_QUEUE, "<moduleName>");
+    c.push(MODULES_IMPORT_QUEUE, "<moduleName>");
 }
 
 void handleImports(Collector c, Tree root, PathConfig pcfg) {
     set[str] imported = {};
-    while (list[str] modulesToImport := c.getStack(__MODULES_IMPORT_QUEUE) && modulesToImport != []) {
-    	c.clearStack(__MODULES_IMPORT_QUEUE);
+    while (list[str] modulesToImport := c.getStack(MODULES_IMPORT_QUEUE) && modulesToImport != []) {
+        c.clearStack(MODULES_IMPORT_QUEUE);
         for (m <- modulesToImport, m notin imported) {
             if (<true, l> := lookupModule(m, pcfg)) {
                 collect(parse(#start[Program], l).top, c);
@@ -121,7 +120,7 @@ void collect(current: (Type) `<Id name>`, Collector c){
 // ----  Examples & Tests --------------------------------
 TModel modulesTModelFromTree(Tree pt){
     if (pt has top) pt = pt.top;
-    c = newCollector("modules", pt, config=getModulesConfig(debug = debug));
+    c = newCollector("modules", pt, getModulesConfig());
     collect(pt, c);
     handleImports(c, pt, pathConfig(pt@\loc));
     return newSolver(pt, c.run()).run();
@@ -143,13 +142,13 @@ public start[Program] sampleModules(str name) = parse(#start[Program], |project:
 
 list[Message] runModules(str name, bool debug = false) {
     Tree pt = sampleModules(name);
-    TModel tm = modulesTModelFromTree(pt, debug = debug);
+    TModel tm = modulesTModelFromTree(pt);
     return tm.messages;
 }
  
 bool testModules(int n, bool debug = false, set[str] runOnly = {}) {
     return runTests([|project://modules-core/src/lang/modules/modules<"<n>">.ttl|], #start[Program], TModel (Tree t) {
-        return modulesTModelFromTree(t, debug=debug);
+        return modulesTModelFromTree(t);
     }, runOnly = runOnly);
 }
 
diff --git a/src/examples/modules/Rename.rsc b/src/examples/modules/Rename.rsc
new file mode 100644
index 00000000..b3f33c7f
--- /dev/null
+++ b/src/examples/modules/Rename.rsc
@@ -0,0 +1,149 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+
+module examples::modules::Rename
+
+import examples::modules::Checker;
+import examples::modules::Syntax;
+
+extend analysis::typepal::refactor::Rename;
+import analysis::diff::edits::TextEdits;
+
+import Exception;
+import IO;
+import Map;
+import Relation;
+import Set;
+import util::FileSystem;
+import util::Maybe;
+import util::Reflective;
+
+data RenameConfig(
+    PathConfig pcfg = pathConfig()
+);
+
+public tuple[list[DocumentEdit] edits, set[Message] msgs] renameModules(list[Tree] cursor, str newName) {
+    bool nameIsValid = any(ModuleId _ <- cursor)
+        ? isValidName(moduleId(), newName)
+        : isValidName(structId(), newName);
+
+    if (!nameIsValid) {
+        return <[], {error("Invalid name: <newName>", cursor[0].src)}>;
+    }
+
+    return rename(
+        cursor
+      , newName
+      , rconfig(
+          Tree(loc l) { return parse(#start[Program], l); }
+        , modulesTModelFromTree
+        , pcfg = pathConfig(srcs = [cursor[0].src.top.parent])
+        , jobLabel = "Renaming \'<cursor[0]>\' to \'<newName>\' at <cursor[0].src>"
+      )
+    );
+}
+
+bool isValidName(moduleId(), str name) = tryParse(#ModuleId, name);
+bool isValidName(structId(), str name) = tryParse(#Id, name);
+
+set[loc] projectFiles(pathConfig(srcs = srcs)) =
+    {*fs | dir <- srcs
+       , fs := find(dir, "modules")};
+
+private Maybe[tuple[IdRole, str]] analyzeUse((Import) `import <ModuleId id>`) = just(<moduleId(), "<id>">);
+private Maybe[tuple[IdRole, str]] analyzeUse((DeclInStruct) `<Type id>`) = just(<structId(), "<id>">);
+private default Maybe[tuple[IdRole, str]] analyzeUse(Tree _) = nothing();
+
+private Maybe[tuple[IdRole, str]] analyzeDef((Program) `module <ModuleId id> <Import* _> <TopLevelDecl* _>`) = just(<moduleId(), "<id>">);
+private Maybe[tuple[IdRole, str]] analyzeDef((TopLevelDecl) `struct <Id id> { <DeclInStruct* _> }`) = just(<structId(), "<id>">);
+private default Maybe[tuple[IdRole, str]] analyzeDef(Tree _) = nothing();
+
+set[Define] getCursorDefinitions(list[Tree] cursor, Tree(loc) getTree, TModel(Tree) getModel, Renamer r) {
+    TModel tm = getModel(cursor[-1]);
+    for (Tree c <- cursor) {
+        if (tm.definitions[c.src]?) {
+            return {tm.definitions[c.src]};
+        } else {
+            str cursorName = "<c>";
+            set[Define] defs = {};
+            for (loc f <- (tm.paths<pathRole, to>)[importPath()]) {
+                Tree fTree = getTree(f);
+                for (/Tree t := fTree) {
+                    if (just(<idRole, cursorName>) := analyzeDef(t)) {
+                        tm = getModel(fTree);
+                        defs += {d | Define d:<_, cursorName, _, idRole, _, _> <- tm.defines};
+                    }
+                }
+            }
+            if (defs != {}) return defs;
+        }
+    }
+
+    r.msg(error(cursor[0].src, "Could not find definition to rename."));
+    return {};
+}
+
+tuple[set[loc], set[loc], set[loc]] findOccurrenceFiles(set[Define] defs, list[Tree] cursor, str newName, Tree(loc) getTree, Renamer r) {
+    set[loc] defFiles = {};
+    set[loc] useFiles = {};
+    set[loc] newNameFiles = {};
+
+    for (Define _:<_, name, _, idRole, _, _> <- defs) {
+        for (loc f <- projectFiles(r.getConfig().pcfg)) {
+            for (/Tree t := getTree(f)) {
+                if (just(<idRole, str n>) := analyzeDef(t)) {
+                    if (n == name) defFiles += f;
+                    else if (n == newName) newNameFiles += f;
+                }
+                if (just(<idRole, str n>) := analyzeUse(t)) {
+                    if (n == name) useFiles += f;
+                    else if (n == newName) newNameFiles += f;
+                }
+            }
+        }
+    }
+
+    return <defFiles, useFiles, newNameFiles>;
+}
+
+void renameUses(set[Define] defs, str newName, TModel tm, Renamer r) {
+    // Somehow, tm.useDef is empty, so we need to use tm.uses
+    rel[loc, loc] defUse = {<d, u> | <Define _:<_, id, orgId, idRole, d, _>, use(id, orgId, u, _, _)> <- defs * toSet(tm.uses)};
+
+    for (loc u <- defUse[defs.defined]) {
+        r.textEdit(replace(u, newName));
+    }
+}
+
+bool tryParse(type[&T <: Tree] tp, str s) {
+    try {
+        parse(tp, s);
+        return true;
+    } catch ParseError(_): {
+        return false;
+    }
+}
diff --git a/src/examples/modules/RenameTest.rsc b/src/examples/modules/RenameTest.rsc
new file mode 100644
index 00000000..33e523dc
--- /dev/null
+++ b/src/examples/modules/RenameTest.rsc
@@ -0,0 +1,92 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module examples::modules::RenameTest
+
+import examples::modules::Rename;
+import examples::modules::Syntax;
+
+import analysis::diff::edits::TextEdits;
+
+import examples::rename::Focus;
+
+import IO;
+import List;
+import ParseTree;
+import Set;
+import String;
+import util::FileSystem;
+
+tuple[list[DocumentEdit] edits, set[Message] msgs] basicRename(str modName, int line, int col, str newName = "foo") {
+    prog = parse(#start[Program], |project://typepal/src/examples/modules/<modName>.modules|);
+    cursor = computeFocusList(prog, line, col);
+    return renameModules(cursor, newName);
+}
+
+void checkNoErrors(set[Message] msgs) {
+    if (m <- msgs, m is error) {
+        throw "Renaming threw errors:\n - <intercalate("\n - ", toList(msgs))>";
+    }
+}
+
+test bool localStructName() {
+    <edits, msgs> = basicRename("C", 6, 9);
+
+    checkNoErrors(msgs);
+    return size(edits) == 1
+        && size(edits[0].edits) == 1;
+}
+
+test bool importedStructName() {
+    <edits, msgs> = basicRename("C", 8, 6);
+
+    checkNoErrors(msgs);
+    return size(edits) == 2
+        && size(edits[0].edits) == 1
+        && size(edits[1].edits) == 1;
+}
+
+test bool moduleName() {
+    <edits, msgs> = basicRename("A", 1, 8);
+
+    checkNoErrors(msgs);
+    return size(edits) == 2
+        && size(edits[0].edits) == 1
+        && size(edits[1].edits) == 1;
+}
+
+// test bool hasFiveChanges() {
+//     <edits, _> = basicRename();
+//     return size(edits) == 1 && size(edits[0].edits) == 5;
+// }
+
+// test bool editsHaveLangthOfNameUnderCursor() {
+//     <edits, _> = basicRename();
+//     for (changed(_, rs) <- edits, replace(loc l, _) <- rs) {
+//         if (size("output") != l.length) return false;
+//     }
+//     return true;
+// }
diff --git a/src/examples/pico/Rename.rsc b/src/examples/pico/Rename.rsc
new file mode 100644
index 00000000..7375b89f
--- /dev/null
+++ b/src/examples/pico/Rename.rsc
@@ -0,0 +1,65 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module examples::pico::Rename
+
+import examples::pico::Syntax;
+import examples::pico::Checker;
+
+import analysis::typepal::TModel;
+
+extend analysis::typepal::refactor::Rename;
+import analysis::diff::edits::TextEdits;
+
+import Exception;
+import IO;
+import Relation;
+import util::FileSystem;
+
+public tuple[list[DocumentEdit] edits, set[Message] msgs] renamePico(list[Tree] cursor, str newName) {
+    if (!isValidName(newName)) {
+        return <[], {error("\'<newName>\' is not a valid name here.", cursor[0].src)}>;
+    }
+
+    return rename(
+        cursor
+      , newName
+      , rconfig(
+          Tree(loc l) { return parse(#start[Program], l); }
+        , collectAndSolve
+        , jobLabel = "Renaming \'<cursor[0]>\' to \'<newName>\' at <cursor[0].src>"
+      )
+    );
+}
+
+bool isValidName(str name) {
+    try {
+        parse(#Id, name);
+        return true;
+    } catch ParseError(_): {
+        return false;
+    }
+}
diff --git a/src/examples/pico/RenameTest.rsc b/src/examples/pico/RenameTest.rsc
new file mode 100644
index 00000000..0a011df4
--- /dev/null
+++ b/src/examples/pico/RenameTest.rsc
@@ -0,0 +1,89 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module examples::pico::RenameTest
+
+import examples::pico::Rename;
+import examples::pico::Syntax;
+
+import analysis::diff::edits::TextEdits;
+
+import examples::rename::Focus;
+
+import IO;
+import List;
+import Message;
+import ParseTree;
+import Set;
+import String;
+import util::FileSystem;
+
+tuple[list[DocumentEdit] edits, set[Message] msgs] basicRename(str newName = "foo", int line = 2, int col = 17) {
+    prog = parse(#start[Program], |project://typepal/src/examples/pico/fac.pico|);
+    cursor = computeFocusList(prog, line, col);
+    return renamePico(cursor, newName);
+}
+
+void checkNoErrors(set[Message] msgs) {
+    if (m <- msgs, m is error) {
+        throw "Renaming threw errors:\n - <intercalate("\n - ", toList(msgs))>";
+    }
+}
+
+test bool doesNotCrash() {
+    <edits, msgs> = basicRename();
+    checkNoErrors(msgs);
+    return true;
+}
+
+test bool hasFiveChanges() {
+    <edits, msgs> = basicRename();
+    checkNoErrors(msgs);
+    return size(edits) == 1 && size(edits[0].edits) == 5;
+}
+
+test bool editsHaveLangthOfNameUnderCursor() {
+    <edits, msgs> = basicRename();
+    checkNoErrors(msgs);
+    for (changed(_, rs) <- edits, replace(loc l, _) <- rs) {
+        if (size("output") != l.length) return false;
+    }
+    return true;
+}
+
+test bool failsWithError() {
+    if (<_, {error(_, _), *_}> := basicRename(col = 26)) {
+        return true;
+    }
+    return false;
+}
+
+test bool invalidName() {
+    if (<_, {error(_, _), *_}> := basicRename(newName = "_foo")) {
+        return true;
+    }
+    return false;
+}
diff --git a/src/examples/rename/Focus.rsc b/src/examples/rename/Focus.rsc
new file mode 100644
index 00000000..d2a38da3
--- /dev/null
+++ b/src/examples/rename/Focus.rsc
@@ -0,0 +1,34 @@
+module examples::rename::Focus
+
+import IO;
+
+import Location;
+import String;
+import ParseTree;
+
+bool isContainedIn(Tree tr, int line, int col) {
+    if (!tr.src?) return false;
+    tuple[int line, int col] begin = tr.src.begin;
+    tuple[int line, int col]  end = tr.src.end;
+
+    if (line < begin.line) return false;
+    if (line > end.line) return false;
+    if (line == begin.line) {
+        if (col < begin.col) return false;
+        if (line == end.line) return col <= end.col; // single line
+        return line <= end.line; // cursor on first line of tree
+    }
+    // line > begin.line && line < end.line
+    if (line == end.line) return col <= end.col; // cursor on last line of tree
+    return true;
+}
+
+public list[Tree] computeFocusList(Tree tr, int line, int col) {
+    list[Tree] focus = [];
+    bottom-up visit(tr) {
+        case t:appl(prod(def, _, _), _): {
+            if (isContainedIn(t, line, col) && trim("<t>") != "") focus += t;
+        }
+    }
+    return focus;
+}


Reviews:
user: login: DavyLandman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T09:02:25Z
user: login: toinehartman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-07T15:51:26Z
user: login: DavyLandman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T12:11:57Z
user: login: toinehartman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T13:34:00Z
user: login: DavyLandman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T13:52:14Z
user: login: PaulKlint
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T13:54:51Z
user: login: PaulKlint
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T13:56:32Z
user: login: PaulKlint
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T14:00:02Z
user: login: toinehartman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T07:54:06Z
user: login: toinehartman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T09:18:57Z
user: login: toinehartman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T09:19:13Z
user: login: toinehartman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T09:37:37Z
user: login: PaulKlint
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-13T21:50:25Z
user: login: DavyLandman
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-15T13:53:17Z

Review comments:
diff_hunk: @@ -0,0 +1,86 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module examples::modfun::RenameTest
+
+import examples::modfun::Rename;
+import examples::modfun::Syntax;
+
+import analysis::typepal::refactor::TextEdits;
+
+import util::LanguageServer; // computeFocusList
path: src/examples/modfun/RenameTest.rsc
user: login: DavyLandman
body: I don't think we should impose a rascal-lsp dependency on typepal.
created_at: 2025-05-07T09:02:25Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,86 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module examples::modfun::RenameTest
+
+import examples::modfun::Rename;
+import examples::modfun::Syntax;
+
+import analysis::typepal::refactor::TextEdits;
+
+import util::LanguageServer; // computeFocusList
path: src/examples/modfun/RenameTest.rsc
user: login: toinehartman
body: Agreed. Fixed by mimicking the (basic) behaviour of this function in Rascal, just for the tests.
created_at: 2025-05-07T15:51:26Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,419 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module analysis::typepal::refactor::Rename
+
+import analysis::diff::edits::TextEdits;
+import analysis::typepal::FailMessage;
+import analysis::typepal::Messenger;
+import analysis::typepal::TModel;
+
+import util::Monitor;
+
+import Exception;
+import IO;
+import List;
+import Map;
+import Message;
+import Node;
+import ParseTree;
+import Relation;
+import Set;
+
+alias RenameResult = tuple[list[DocumentEdit], set[Message]];
+alias Focus = list[Tree];
+
+// This leaves some room for more fine-grained steps should the user want to monitor that
+private int WORKSPACE_WORK = 10;
+private int FILE_WORK = 5;
+
+data Renamer
+    = renamer(
+        void(FailMessage) msg
+      , void(DocumentEdit) documentEdit
+      , void(TextEdit) textEdit
+      , RenameConfig() getConfig
+
+      // Helpers
+      , void(value, str) warning
+      , void(value, str) info
+      , void(value, str) error
path: src/analysis/typepal/refactor/Rename.rsc
user: login: DavyLandman
body: I think we should either remove these, or align them to: https://github.com/usethesource/typepal/blob/0b73d5e381acd5508f8a672bdc003152f61f0935/src/analysis/typepal/FailMessage.rsc#L29-L34 (at least the var-args)
created_at: 2025-05-12T12:11:56Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,419 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module analysis::typepal::refactor::Rename
+
+import analysis::diff::edits::TextEdits;
+import analysis::typepal::FailMessage;
+import analysis::typepal::Messenger;
+import analysis::typepal::TModel;
+
+import util::Monitor;
+
+import Exception;
+import IO;
+import List;
+import Map;
+import Message;
+import Node;
+import ParseTree;
+import Relation;
+import Set;
+
+alias RenameResult = tuple[list[DocumentEdit], set[Message]];
+alias Focus = list[Tree];
+
+// This leaves some room for more fine-grained steps should the user want to monitor that
+private int WORKSPACE_WORK = 10;
+private int FILE_WORK = 5;
+
+data Renamer
+    = renamer(
+        void(FailMessage) msg
+      , void(DocumentEdit) documentEdit
+      , void(TextEdit) textEdit
+      , RenameConfig() getConfig
+
+      // Helpers
+      , void(value, str) warning
+      , void(value, str) info
+      , void(value, str) error
path: src/analysis/typepal/refactor/Rename.rsc
user: login: toinehartman
body: I believe varargs are not included in the syntax of `Type`, and that's why I left them out. We can choose to leave out these shortcuts completely; then `r.error(|some:///|, "error");` will become
```
import analysis::typepal::FailMessage;
r.msg(error(|some:///|, "error"));
```

That constructor has its arguments flipped compared to `util::Message`, btw, which might lead to more confusion. https://www.rascal-mpl.org/docs/Library/Message/
created_at: 2025-05-12T13:34:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,419 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module analysis::typepal::refactor::Rename
+
+import analysis::diff::edits::TextEdits;
+import analysis::typepal::FailMessage;
+import analysis::typepal::Messenger;
+import analysis::typepal::TModel;
+
+import util::Monitor;
+
+import Exception;
+import IO;
+import List;
+import Map;
+import Message;
+import Node;
+import ParseTree;
+import Relation;
+import Set;
+
+alias RenameResult = tuple[list[DocumentEdit], set[Message]];
+alias Focus = list[Tree];
+
+// This leaves some room for more fine-grained steps should the user want to monitor that
+private int WORKSPACE_WORK = 10;
+private int FILE_WORK = 5;
+
+data Renamer
+    = renamer(
+        void(FailMessage) msg
+      , void(DocumentEdit) documentEdit
+      , void(TextEdit) textEdit
+      , RenameConfig() getConfig
+
+      // Helpers
+      , void(value, str) warning
+      , void(value, str) info
+      , void(value, str) error
path: src/analysis/typepal/refactor/Rename.rsc
user: login: DavyLandman
body: yes, that flip is a bit confusing..
created_at: 2025-05-12T13:52:14Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,419 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module analysis::typepal::refactor::Rename
+
+import analysis::diff::edits::TextEdits;
+import analysis::typepal::FailMessage;
+import analysis::typepal::Messenger;
+import analysis::typepal::TModel;
+
+import util::Monitor;
+
+import Exception;
+import IO;
+import List;
+import Map;
+import Message;
+import Node;
+import ParseTree;
+import Relation;
+import Set;
+
+alias RenameResult = tuple[list[DocumentEdit], set[Message]];
+alias Focus = list[Tree];
+
+// This leaves some room for more fine-grained steps should the user want to monitor that
+private int WORKSPACE_WORK = 10;
+private int FILE_WORK = 5;
+
+data Renamer
+    = renamer(
+        void(FailMessage) msg
+      , void(DocumentEdit) documentEdit
+      , void(TextEdit) textEdit
+      , RenameConfig() getConfig
+
+      // Helpers
+      , void(value, str) warning
+      , void(value, str) info
+      , void(value, str) error
+    );
+
+@synopsis{Language-specific configuration of identifier renaming.}
+@description{
+Configures the rename for a specific language, by giving the following mandatory arguments:
+* `Tree parseLoc(loc l)`, which parses a file and returns a ((ParseTree-Tree)). Typically this would be `parse(#StartSymbol, l)`.
+* `TModel tmodelForTree(Tree t)`, which type-checks a ((ParseTree-Tree)) and returns a ((TModel)). Typically, this would be ((collectAndSolve)).
+The configuration also takes some optional arguments:
+* `TModel tmodelForLoc(loc l)`, which type-checks a file given its `loc` instead of its parse tree. This can be useful when the file does not have to be parsed to produce a ((TModel)), for example when it had already been type-checked just before starting the renaming. Defaults to `tmodelForTree(parseLoc(l))`.
+* `bool debug`, which indicates whether debug output should be printed during the renaming. Defaults to `false`.
+*`str jobLabel`, which can be used to change the base label of the rename progress bar. Defaults to `"Renaming"`.
+}
+data RenameConfig
+    = rconfig(
+        Tree(loc) parseLoc
+      , TModel(Tree) tmodelForTree
+      , TModel(loc) tmodelForLoc = TModel(loc l) { return tmodelForTree(parseLoc(l)); }
+      , bool debug = false
+      , str jobLabel = "Renaming"
+    );
+
+@synopsis{Sorts ((analysis::diff::edits::TextEdits::DocumentEdit))s.}
+@description{
+Applying edits through ((analysis::diff::edits::ExecuteTextEdits)) should happen in a specific order.
+Specifically, files should be created before they can be modified, and after renaming them, modifications/deletions should refer to the new name.
+This functions sorts edits in the following order.
+    1. created
+    2. ((changed))
+    3. ((renamed))
+    4. ((removed))
+}
+list[DocumentEdit] sortDocEdits(list[DocumentEdit] edits) = sort(edits, bool(DocumentEdit e1, DocumentEdit e2) {
+    if (e1 is created && !(e2 is created)) return true;
+    if (e1 is changed && !(e2 is changed)) return !(e2 is created);
+    if (e1 is renamed && !(e2 is renamed)) return (e2 is removed);
+    return false;
+});
+
+@synopsis{Renames the identifier under the cursor to `newName` and returns the required ((analysis::diff::edits::TextEdits-DocumentEdit))s and ((Message-Message))s.}
+@description{
+Renames the identifier under the cursor (represented as a ((Focus))) to `newName`, given a specific ((analysis::typepal::refactor::Rename-RenameConfig)).
+This renaming uses ((TModel))s produced by the type-checker.
+
+The rename framework provides a default implementation, which can be selectively extended for languages that require different rename behaviour than the default. These functions constitute the implementation of renaming:
+* ((analysis::typepal::refactor::Rename-getCursorDefinitions))
+* ((analysis::typepal::refactor::Rename-findOccurrenceFiles))
+* ((analysis::typepal::refactor::Rename-findAdditionalDefinitions))
+* ((analysis::typepal::refactor::Rename-validateNewNameOccurrences))
+* ((analysis::typepal::refactor::Rename-renameDefinition))
+* ((analysis::typepal::refactor::Rename-renameUses))
+* ((analysis::typepal::refactor::Rename-nameLocation))
+}
+@pitfalls{
+* Since the ((analysis::typepal::refactor::Rename-RenameConfig)) that this function passes to the various hooks contains state and caches, it should never escape the scope of a single invoation of ((rename)). Instead, it should always be accessed via ((renamer))'s `getConfig`.
+* The renaming highly depends on a type-check function `TModel(Tree)`. If such a function does not exist, this framework cannot function.
+}
+RenameResult rename(
+        Focus cursor
+      , str newName
+      , RenameConfig config) {
+
+    jobStart(config.jobLabel, totalWork = 2 * WORKSPACE_WORK + 1);
+    jobStep(config.jobLabel, "Initializing renaming");
+
+    // Tree & TModel caching
+
+    @memo{maximumSize(50)}
+    TModel getTModelCached(Tree t) {
+        tm = config.tmodelForTree(t);
+        if (msg <- tm.messages, msg is error) registerMessage(error(t.src.top, "Renaming failed, since this file has type error(s)."));
+        return tm;
+    }
+
+    @memo{maximumSize(50)}
+    TModel getTModelForLocCached(loc l) = config.tmodelForLoc(l);
+
+    @memo{maximumSize(50)}
+    Tree parseLocCached(loc l) {
+        // We already have the parse tree of the module under cursor
+        if (l == cursor[-1].src.top) {
+            return cursor[-1];
+        }
+
+        try {
+            return config.parseLoc(l);
+        } catch ParseError(_): {
+            registerMessage(error(l, "Renaming failed, since an error occurred while parsing this file."));
+            return char(-1);
+        }
+    }
path: src/analysis/typepal/refactor/Rename.rsc
user: login: PaulKlint
body: We krijgen hier een soort tweetraps caching. Op termijn erover nadenken hoe we deze caching en die uit de Rascal typechecker kunnen integreren?
created_at: 2025-05-12T13:54:50Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,419 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module analysis::typepal::refactor::Rename
+
+import analysis::diff::edits::TextEdits;
+import analysis::typepal::FailMessage;
+import analysis::typepal::Messenger;
+import analysis::typepal::TModel;
+
+import util::Monitor;
+
+import Exception;
+import IO;
+import List;
+import Map;
+import Message;
+import Node;
+import ParseTree;
+import Relation;
+import Set;
+
+alias RenameResult = tuple[list[DocumentEdit], set[Message]];
+alias Focus = list[Tree];
+
+// This leaves some room for more fine-grained steps should the user want to monitor that
+private int WORKSPACE_WORK = 10;
+private int FILE_WORK = 5;
+
+data Renamer
+    = renamer(
+        void(FailMessage) msg
+      , void(DocumentEdit) documentEdit
+      , void(TextEdit) textEdit
+      , RenameConfig() getConfig
+
+      // Helpers
+      , void(value, str) warning
+      , void(value, str) info
+      , void(value, str) error
+    );
+
+@synopsis{Language-specific configuration of identifier renaming.}
+@description{
+Configures the rename for a specific language, by giving the following mandatory arguments:
+* `Tree parseLoc(loc l)`, which parses a file and returns a ((ParseTree-Tree)). Typically this would be `parse(#StartSymbol, l)`.
+* `TModel tmodelForTree(Tree t)`, which type-checks a ((ParseTree-Tree)) and returns a ((TModel)). Typically, this would be ((collectAndSolve)).
+The configuration also takes some optional arguments:
+* `TModel tmodelForLoc(loc l)`, which type-checks a file given its `loc` instead of its parse tree. This can be useful when the file does not have to be parsed to produce a ((TModel)), for example when it had already been type-checked just before starting the renaming. Defaults to `tmodelForTree(parseLoc(l))`.
+* `bool debug`, which indicates whether debug output should be printed during the renaming. Defaults to `false`.
+*`str jobLabel`, which can be used to change the base label of the rename progress bar. Defaults to `"Renaming"`.
+}
+data RenameConfig
+    = rconfig(
+        Tree(loc) parseLoc
+      , TModel(Tree) tmodelForTree
+      , TModel(loc) tmodelForLoc = TModel(loc l) { return tmodelForTree(parseLoc(l)); }
+      , bool debug = false
+      , str jobLabel = "Renaming"
+    );
+
+@synopsis{Sorts ((analysis::diff::edits::TextEdits::DocumentEdit))s.}
+@description{
+Applying edits through ((analysis::diff::edits::ExecuteTextEdits)) should happen in a specific order.
+Specifically, files should be created before they can be modified, and after renaming them, modifications/deletions should refer to the new name.
+This functions sorts edits in the following order.
+    1. created
+    2. ((changed))
+    3. ((renamed))
+    4. ((removed))
+}
+list[DocumentEdit] sortDocEdits(list[DocumentEdit] edits) = sort(edits, bool(DocumentEdit e1, DocumentEdit e2) {
+    if (e1 is created && !(e2 is created)) return true;
+    if (e1 is changed && !(e2 is changed)) return !(e2 is created);
+    if (e1 is renamed && !(e2 is renamed)) return (e2 is removed);
+    return false;
+});
+
+@synopsis{Renames the identifier under the cursor to `newName` and returns the required ((analysis::diff::edits::TextEdits-DocumentEdit))s and ((Message-Message))s.}
+@description{
+Renames the identifier under the cursor (represented as a ((Focus))) to `newName`, given a specific ((analysis::typepal::refactor::Rename-RenameConfig)).
+This renaming uses ((TModel))s produced by the type-checker.
+
+The rename framework provides a default implementation, which can be selectively extended for languages that require different rename behaviour than the default. These functions constitute the implementation of renaming:
+* ((analysis::typepal::refactor::Rename-getCursorDefinitions))
+* ((analysis::typepal::refactor::Rename-findOccurrenceFiles))
+* ((analysis::typepal::refactor::Rename-findAdditionalDefinitions))
+* ((analysis::typepal::refactor::Rename-validateNewNameOccurrences))
+* ((analysis::typepal::refactor::Rename-renameDefinition))
+* ((analysis::typepal::refactor::Rename-renameUses))
+* ((analysis::typepal::refactor::Rename-nameLocation))
+}
+@pitfalls{
+* Since the ((analysis::typepal::refactor::Rename-RenameConfig)) that this function passes to the various hooks contains state and caches, it should never escape the scope of a single invoation of ((rename)). Instead, it should always be accessed via ((renamer))'s `getConfig`.
+* The renaming highly depends on a type-check function `TModel(Tree)`. If such a function does not exist, this framework cannot function.
+}
+RenameResult rename(
+        Focus cursor
+      , str newName
+      , RenameConfig config) {
+
+    jobStart(config.jobLabel, totalWork = 2 * WORKSPACE_WORK + 1);
+    jobStep(config.jobLabel, "Initializing renaming");
+
+    // Tree & TModel caching
+
+    @memo{maximumSize(50)}
+    TModel getTModelCached(Tree t) {
+        tm = config.tmodelForTree(t);
+        if (msg <- tm.messages, msg is error) registerMessage(error(t.src.top, "Renaming failed, since this file has type error(s)."));
+        return tm;
+    }
+
+    @memo{maximumSize(50)}
+    TModel getTModelForLocCached(loc l) = config.tmodelForLoc(l);
+
+    @memo{maximumSize(50)}
+    Tree parseLocCached(loc l) {
+        // We already have the parse tree of the module under cursor
+        if (l == cursor[-1].src.top) {
+            return cursor[-1];
+        }
+
+        try {
+            return config.parseLoc(l);
+        } catch ParseError(_): {
+            registerMessage(error(l, "Renaming failed, since an error occurred while parsing this file."));
+            return char(-1);
+        }
+    }
+
+    // Make sure user uses cached functions
+    cachedConfig = config
+        [parseLoc = parseLocCached]
+        [tmodelForTree = getTModelCached]
+        [tmodelForLoc = getTModelForLocCached]
+        ;
+
+    // Messages
+    set[FailMessage] messages = {};
+    bool errorReported() = messages != {} && any(m <- messages, m is fm_error);
+    void registerMessage(FailMessage msg) { messages += msg; };
+    AType getType(Tree t) {
+        TModel tm = getTModelCached(parseLocCached(t.src.top));
+        if (tm.facts[t.src]?) {
+            return tm.facts[t.src];
+        }
+
+        return tvar(|unknown:///|);
+    }
+    set[Message] getMessages() = {toMessage(m, getType) | m <- messages};
+
+    // Edits
+    set[loc] editsSeen = {};
+    list[DocumentEdit] docEdits = [];
+
+    void checkEdit(replace(loc range, _)) {
+        if (range in editsSeen) {
+            registerMessage(error(range, "Multiple replace edits for this location."));
+        }
+        editsSeen += range;
+    }
+
+    void checkEdit(DocumentEdit e) {
+        loc file = e has file ? e.file : e.from;
+        if (changed(f, tes) := e) {
+            // Check contents of DocumentEdit
+            for (te:replace(range, _) <- tes) {
+                // Check integrity
+                if (range.top != f) {
+                    registerMessage(error(range, "Invalid replace edit for this location. This location is not in <f>, for which it was registered."));
+                }
+
+                // Check text edits
+                checkEdit(te);
+            }
+        } else if (file in editsSeen) {
+            registerMessage(error(file, "Multiple <getName(e)> edits for this file."));
+        }
+
+        editsSeen += file;
+    }
+
+    void registerDocumentEdit(DocumentEdit e) {
+        checkEdit(e);
+        docEdits += e;
+    };
+
+    void registerTextEdit(TextEdit e) {
+        checkEdit(e);
+
+        loc f = e.range.top;
+        if ([*pre, c:changed(f, _)] := docEdits) {
+            // If possible, merge with latest document edit
+            // TODO Just assign to docEdits[-1], once this issue has been solved:
+            // https://github.com/usethesource/rascal/issues/2123
+            docEdits = [*pre, c[edits = c.edits + e]];
+        } else {
+            // Else, create new document edit
+            docEdits += changed(f, [e]);
+        }
+    };
+
+    Renamer r = renamer(
+        registerMessage
+      , registerDocumentEdit
+      , registerTextEdit
+      , RenameConfig() { return cachedConfig; }
+      , void(value at, str s) { registerMessage(info(at, s)); }
+      , void(value at, str s) { registerMessage(warning(at, s)); }
+      , void(value at, str s) { registerMessage(error(at, s)); }
+    );
+
+    jobStep(config.jobLabel, "Resolving definitions of <cursor[0].src>", work = WORKSPACE_WORK);
+    defs = getCursorDefinitions(cursor, parseLocCached, getTModelCached, r);
+
+    if (defs == {}) r.error(cursor[0].src, "No definitions found");
+    if (errorReported()) {
+        jobEnd(config.jobLabel, success=false);
+        return <sortDocEdits(docEdits), getMessages()>;
+    }
+
+    jobStep(config.jobLabel, "Looking for files with occurrences of name under cursor", work = WORKSPACE_WORK);
+    <maybeDefFiles, maybeUseFiles, newNameFiles> = findOccurrenceFiles(defs, cursor, newName, parseLocCached, r);
+
+    jobTodo(config.jobLabel, work = (size(maybeDefFiles) + size(maybeUseFiles) + size(newNameFiles)) * FILE_WORK);
+
+    set[Define] additionalDefs = {};
+    solve (additionalDefs) {
+        for (loc f <- maybeDefFiles) {
+            jobStep(config.jobLabel, "Looking for additional definitions in <f>", work = 0);
+            tr = parseLocCached(f);
+            tm = getTModelCached(tr);
+            additionalDefs += findAdditionalDefinitions(defs, tr, tm, r);
+        }
+        defs += additionalDefs;
+    }
+    jobStep(config.jobLabel, "Done looking for additional definitions", work = FILE_WORK * size(maybeDefFiles));
+
+    for (loc f <- newNameFiles) {
+        jobStep(config.jobLabel, "Validating occurrences of new name \'<newName>\' in <f>", work = FILE_WORK);
+        tr = parseLocCached(f);
+        validateNewNameOccurrences(defs, newName, tr, r);
+    }
+    if (errorReported()) {
+        jobEnd(config.jobLabel, success = false);
+        return <sortDocEdits(docEdits), getMessages()>;
+    }
+
+    defFiles = {d.defined.top | d <- defs};
+    jobTodo(config.jobLabel, work = size(defFiles) * FILE_WORK);
+
+    for (loc f <- defFiles) {
+        fileDefs = {d | d <- defs, d.defined.top == f};
+        jobStep(config.jobLabel, "Renaming <size(fileDefs)> definitions in <f>", work = FILE_WORK);
+        tr = parseLocCached(f);
+        tm = getTModelCached(tr);
+
+        map[Define, loc] defNames = defNameLocations(tr, fileDefs, r);
+        for (d <- fileDefs) {
+            renameDefinition(d, defNames[d] ? d.defined, newName, tm, r);
+        }
+    }
+
+    if (errorReported()) {
+        jobEnd(config.jobLabel, success=false);
+        return <sortDocEdits(docEdits), getMessages()>;
+    }
+
+    for (loc f <- maybeUseFiles) {
+        jobStep(config.jobLabel, "Renaming uses in <f>", work = FILE_WORK);
+        tr = parseLocCached(f);
+        tm = getTModelCached(tr);
+
+        renameUses(defs, newName, tm, r);
+    }
+
+    set[Message] convertedMessages = getMessages();
+
+    if (config.debug) {
+        println("\n\n=================\nRename statistics\n=================\n");
+        int nDocs = size({f | de <- docEdits, f := (de has file ? de.file : de.from)});
+        int nEdits = (0 | it + ((changed(_, tes) := e) ? size(tes) : 1) | e <- docEdits);
+
+        int nErrors = size({msg | msg <- convertedMessages, msg is error});
+        int nWarnings = size({msg | msg <- convertedMessages, msg is warning});
+        int nInfos = size({msg | msg <- convertedMessages, msg is info});
+
+        println(" # of documents affected: <nDocs>");
+        println(" # of text edits:         <nEdits>");
+        println(" # of messages:           <size(convertedMessages)>");
+        println("   (<nErrors> errors, <nWarnings> warnings and <nInfos> infos)");
+
+        if (size(convertedMessages) > 0) {
+            println("\n===============\nMessages\n===============");
+            for (msg <- convertedMessages) {
+                println(" ** <msg>");
+            }
+            println();
+        }
+    }
+
+    jobEnd(config.jobLabel, success = !errorReported());
+    return <sortDocEdits(docEdits), convertedMessages>;
+}
+
+// Workaround to be able to pattern match on the emulated `src` field
+data Tree (loc src = |unknown:///|(0,0,<0,0>,<0,0>));
path: src/analysis/typepal/refactor/Rename.rsc
user: login: PaulKlint
body: Dit is erg delicaat totdat de annotaties weg zijn. Als het kan zou ik dit weglaten.
created_at: 2025-05-12T13:56:31Z
author_association: MEMBER
diff_hunk: @@ -43,10 +48,60 @@ void collect(current: (ModuleDecl) `module <ModId mid> { <Decl* decls> }`, Colle
 
 void collect(current: (ImportDecl) `import <ModId mid> ;`, Collector c){
      c.addPathToDef(mid, {moduleId()}, importPath());
+     c.push(MODULES_IMPORT_QUEUE, "<mid>");
path: src/examples/modfun/Checker.rsc
user: login: PaulKlint
body: Sanity check: gaat dit goed voor circulaire imports?
created_at: 2025-05-12T14:00:00Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,419 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module analysis::typepal::refactor::Rename
+
+import analysis::diff::edits::TextEdits;
+import analysis::typepal::FailMessage;
+import analysis::typepal::Messenger;
+import analysis::typepal::TModel;
+
+import util::Monitor;
+
+import Exception;
+import IO;
+import List;
+import Map;
+import Message;
+import Node;
+import ParseTree;
+import Relation;
+import Set;
+
+alias RenameResult = tuple[list[DocumentEdit], set[Message]];
+alias Focus = list[Tree];
+
+// This leaves some room for more fine-grained steps should the user want to monitor that
+private int WORKSPACE_WORK = 10;
+private int FILE_WORK = 5;
+
+data Renamer
+    = renamer(
+        void(FailMessage) msg
+      , void(DocumentEdit) documentEdit
+      , void(TextEdit) textEdit
+      , RenameConfig() getConfig
+
+      // Helpers
+      , void(value, str) warning
+      , void(value, str) info
+      , void(value, str) error
+    );
+
+@synopsis{Language-specific configuration of identifier renaming.}
+@description{
+Configures the rename for a specific language, by giving the following mandatory arguments:
+* `Tree parseLoc(loc l)`, which parses a file and returns a ((ParseTree-Tree)). Typically this would be `parse(#StartSymbol, l)`.
+* `TModel tmodelForTree(Tree t)`, which type-checks a ((ParseTree-Tree)) and returns a ((TModel)). Typically, this would be ((collectAndSolve)).
+The configuration also takes some optional arguments:
+* `TModel tmodelForLoc(loc l)`, which type-checks a file given its `loc` instead of its parse tree. This can be useful when the file does not have to be parsed to produce a ((TModel)), for example when it had already been type-checked just before starting the renaming. Defaults to `tmodelForTree(parseLoc(l))`.
+* `bool debug`, which indicates whether debug output should be printed during the renaming. Defaults to `false`.
+*`str jobLabel`, which can be used to change the base label of the rename progress bar. Defaults to `"Renaming"`.
+}
+data RenameConfig
+    = rconfig(
+        Tree(loc) parseLoc
+      , TModel(Tree) tmodelForTree
+      , TModel(loc) tmodelForLoc = TModel(loc l) { return tmodelForTree(parseLoc(l)); }
+      , bool debug = false
+      , str jobLabel = "Renaming"
+    );
+
+@synopsis{Sorts ((analysis::diff::edits::TextEdits::DocumentEdit))s.}
+@description{
+Applying edits through ((analysis::diff::edits::ExecuteTextEdits)) should happen in a specific order.
+Specifically, files should be created before they can be modified, and after renaming them, modifications/deletions should refer to the new name.
+This functions sorts edits in the following order.
+    1. created
+    2. ((changed))
+    3. ((renamed))
+    4. ((removed))
+}
+list[DocumentEdit] sortDocEdits(list[DocumentEdit] edits) = sort(edits, bool(DocumentEdit e1, DocumentEdit e2) {
+    if (e1 is created && !(e2 is created)) return true;
+    if (e1 is changed && !(e2 is changed)) return !(e2 is created);
+    if (e1 is renamed && !(e2 is renamed)) return (e2 is removed);
+    return false;
+});
+
+@synopsis{Renames the identifier under the cursor to `newName` and returns the required ((analysis::diff::edits::TextEdits-DocumentEdit))s and ((Message-Message))s.}
+@description{
+Renames the identifier under the cursor (represented as a ((Focus))) to `newName`, given a specific ((analysis::typepal::refactor::Rename-RenameConfig)).
+This renaming uses ((TModel))s produced by the type-checker.
+
+The rename framework provides a default implementation, which can be selectively extended for languages that require different rename behaviour than the default. These functions constitute the implementation of renaming:
+* ((analysis::typepal::refactor::Rename-getCursorDefinitions))
+* ((analysis::typepal::refactor::Rename-findOccurrenceFiles))
+* ((analysis::typepal::refactor::Rename-findAdditionalDefinitions))
+* ((analysis::typepal::refactor::Rename-validateNewNameOccurrences))
+* ((analysis::typepal::refactor::Rename-renameDefinition))
+* ((analysis::typepal::refactor::Rename-renameUses))
+* ((analysis::typepal::refactor::Rename-nameLocation))
+}
+@pitfalls{
+* Since the ((analysis::typepal::refactor::Rename-RenameConfig)) that this function passes to the various hooks contains state and caches, it should never escape the scope of a single invoation of ((rename)). Instead, it should always be accessed via ((renamer))'s `getConfig`.
+* The renaming highly depends on a type-check function `TModel(Tree)`. If such a function does not exist, this framework cannot function.
+}
+RenameResult rename(
+        Focus cursor
+      , str newName
+      , RenameConfig config) {
+
+    jobStart(config.jobLabel, totalWork = 2 * WORKSPACE_WORK + 1);
+    jobStep(config.jobLabel, "Initializing renaming");
+
+    // Tree & TModel caching
+
+    @memo{maximumSize(50)}
+    TModel getTModelCached(Tree t) {
+        tm = config.tmodelForTree(t);
+        if (msg <- tm.messages, msg is error) registerMessage(error(t.src.top, "Renaming failed, since this file has type error(s)."));
+        return tm;
+    }
+
+    @memo{maximumSize(50)}
+    TModel getTModelForLocCached(loc l) = config.tmodelForLoc(l);
+
+    @memo{maximumSize(50)}
+    Tree parseLocCached(loc l) {
+        // We already have the parse tree of the module under cursor
+        if (l == cursor[-1].src.top) {
+            return cursor[-1];
+        }
+
+        try {
+            return config.parseLoc(l);
+        } catch ParseError(_): {
+            registerMessage(error(l, "Renaming failed, since an error occurred while parsing this file."));
+            return char(-1);
+        }
+    }
path: src/analysis/typepal/refactor/Rename.rsc
user: login: toinehartman
body: Kunnen we zeker over nadenken.
Maar: dat is alleen zo voor Rascal. Niet voor de gemiddelde DSL die d.m.v. `collectAndSolve` een in-memory TModel bouwt, zonder dit op disk te cachen. Daar is deze `@memo` voor.
created_at: 2025-05-13T07:54:06Z
author_association: MEMBER
diff_hunk: @@ -43,10 +48,60 @@ void collect(current: (ModuleDecl) `module <ModId mid> { <Decl* decls> }`, Colle
 
 void collect(current: (ImportDecl) `import <ModId mid> ;`, Collector c){
      c.addPathToDef(mid, {moduleId()}, importPath());
+     c.push(MODULES_IMPORT_QUEUE, "<mid>");
path: src/examples/modfun/Checker.rsc
user: login: toinehartman
body: Daar lijkt het wel op. Heb dit voor de zekerheid toegevoegd aan de test-modules.
created_at: 2025-05-13T09:18:57Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,419 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module analysis::typepal::refactor::Rename
+
+import analysis::diff::edits::TextEdits;
+import analysis::typepal::FailMessage;
+import analysis::typepal::Messenger;
+import analysis::typepal::TModel;
+
+import util::Monitor;
+
+import Exception;
+import IO;
+import List;
+import Map;
+import Message;
+import Node;
+import ParseTree;
+import Relation;
+import Set;
+
+alias RenameResult = tuple[list[DocumentEdit], set[Message]];
+alias Focus = list[Tree];
+
+// This leaves some room for more fine-grained steps should the user want to monitor that
+private int WORKSPACE_WORK = 10;
+private int FILE_WORK = 5;
+
+data Renamer
+    = renamer(
+        void(FailMessage) msg
+      , void(DocumentEdit) documentEdit
+      , void(TextEdit) textEdit
+      , RenameConfig() getConfig
+
+      // Helpers
+      , void(value, str) warning
+      , void(value, str) info
+      , void(value, str) error
+    );
+
+@synopsis{Language-specific configuration of identifier renaming.}
+@description{
+Configures the rename for a specific language, by giving the following mandatory arguments:
+* `Tree parseLoc(loc l)`, which parses a file and returns a ((ParseTree-Tree)). Typically this would be `parse(#StartSymbol, l)`.
+* `TModel tmodelForTree(Tree t)`, which type-checks a ((ParseTree-Tree)) and returns a ((TModel)). Typically, this would be ((collectAndSolve)).
+The configuration also takes some optional arguments:
+* `TModel tmodelForLoc(loc l)`, which type-checks a file given its `loc` instead of its parse tree. This can be useful when the file does not have to be parsed to produce a ((TModel)), for example when it had already been type-checked just before starting the renaming. Defaults to `tmodelForTree(parseLoc(l))`.
+* `bool debug`, which indicates whether debug output should be printed during the renaming. Defaults to `false`.
+*`str jobLabel`, which can be used to change the base label of the rename progress bar. Defaults to `"Renaming"`.
+}
+data RenameConfig
+    = rconfig(
+        Tree(loc) parseLoc
+      , TModel(Tree) tmodelForTree
+      , TModel(loc) tmodelForLoc = TModel(loc l) { return tmodelForTree(parseLoc(l)); }
+      , bool debug = false
+      , str jobLabel = "Renaming"
+    );
+
+@synopsis{Sorts ((analysis::diff::edits::TextEdits::DocumentEdit))s.}
+@description{
+Applying edits through ((analysis::diff::edits::ExecuteTextEdits)) should happen in a specific order.
+Specifically, files should be created before they can be modified, and after renaming them, modifications/deletions should refer to the new name.
+This functions sorts edits in the following order.
+    1. created
+    2. ((changed))
+    3. ((renamed))
+    4. ((removed))
+}
+list[DocumentEdit] sortDocEdits(list[DocumentEdit] edits) = sort(edits, bool(DocumentEdit e1, DocumentEdit e2) {
+    if (e1 is created && !(e2 is created)) return true;
+    if (e1 is changed && !(e2 is changed)) return !(e2 is created);
+    if (e1 is renamed && !(e2 is renamed)) return (e2 is removed);
+    return false;
+});
+
+@synopsis{Renames the identifier under the cursor to `newName` and returns the required ((analysis::diff::edits::TextEdits-DocumentEdit))s and ((Message-Message))s.}
+@description{
+Renames the identifier under the cursor (represented as a ((Focus))) to `newName`, given a specific ((analysis::typepal::refactor::Rename-RenameConfig)).
+This renaming uses ((TModel))s produced by the type-checker.
+
+The rename framework provides a default implementation, which can be selectively extended for languages that require different rename behaviour than the default. These functions constitute the implementation of renaming:
+* ((analysis::typepal::refactor::Rename-getCursorDefinitions))
+* ((analysis::typepal::refactor::Rename-findOccurrenceFiles))
+* ((analysis::typepal::refactor::Rename-findAdditionalDefinitions))
+* ((analysis::typepal::refactor::Rename-validateNewNameOccurrences))
+* ((analysis::typepal::refactor::Rename-renameDefinition))
+* ((analysis::typepal::refactor::Rename-renameUses))
+* ((analysis::typepal::refactor::Rename-nameLocation))
+}
+@pitfalls{
+* Since the ((analysis::typepal::refactor::Rename-RenameConfig)) that this function passes to the various hooks contains state and caches, it should never escape the scope of a single invoation of ((rename)). Instead, it should always be accessed via ((renamer))'s `getConfig`.
+* The renaming highly depends on a type-check function `TModel(Tree)`. If such a function does not exist, this framework cannot function.
+}
+RenameResult rename(
+        Focus cursor
+      , str newName
+      , RenameConfig config) {
+
+    jobStart(config.jobLabel, totalWork = 2 * WORKSPACE_WORK + 1);
+    jobStep(config.jobLabel, "Initializing renaming");
+
+    // Tree & TModel caching
+
+    @memo{maximumSize(50)}
+    TModel getTModelCached(Tree t) {
+        tm = config.tmodelForTree(t);
+        if (msg <- tm.messages, msg is error) registerMessage(error(t.src.top, "Renaming failed, since this file has type error(s)."));
+        return tm;
+    }
+
+    @memo{maximumSize(50)}
+    TModel getTModelForLocCached(loc l) = config.tmodelForLoc(l);
+
+    @memo{maximumSize(50)}
+    Tree parseLocCached(loc l) {
+        // We already have the parse tree of the module under cursor
+        if (l == cursor[-1].src.top) {
+            return cursor[-1];
+        }
+
+        try {
+            return config.parseLoc(l);
+        } catch ParseError(_): {
+            registerMessage(error(l, "Renaming failed, since an error occurred while parsing this file."));
+            return char(-1);
+        }
+    }
+
+    // Make sure user uses cached functions
+    cachedConfig = config
+        [parseLoc = parseLocCached]
+        [tmodelForTree = getTModelCached]
+        [tmodelForLoc = getTModelForLocCached]
+        ;
+
+    // Messages
+    set[FailMessage] messages = {};
+    bool errorReported() = messages != {} && any(m <- messages, m is fm_error);
+    void registerMessage(FailMessage msg) { messages += msg; };
+    AType getType(Tree t) {
+        TModel tm = getTModelCached(parseLocCached(t.src.top));
+        if (tm.facts[t.src]?) {
+            return tm.facts[t.src];
+        }
+
+        return tvar(|unknown:///|);
+    }
+    set[Message] getMessages() = {toMessage(m, getType) | m <- messages};
+
+    // Edits
+    set[loc] editsSeen = {};
+    list[DocumentEdit] docEdits = [];
+
+    void checkEdit(replace(loc range, _)) {
+        if (range in editsSeen) {
+            registerMessage(error(range, "Multiple replace edits for this location."));
+        }
+        editsSeen += range;
+    }
+
+    void checkEdit(DocumentEdit e) {
+        loc file = e has file ? e.file : e.from;
+        if (changed(f, tes) := e) {
+            // Check contents of DocumentEdit
+            for (te:replace(range, _) <- tes) {
+                // Check integrity
+                if (range.top != f) {
+                    registerMessage(error(range, "Invalid replace edit for this location. This location is not in <f>, for which it was registered."));
+                }
+
+                // Check text edits
+                checkEdit(te);
+            }
+        } else if (file in editsSeen) {
+            registerMessage(error(file, "Multiple <getName(e)> edits for this file."));
+        }
+
+        editsSeen += file;
+    }
+
+    void registerDocumentEdit(DocumentEdit e) {
+        checkEdit(e);
+        docEdits += e;
+    };
+
+    void registerTextEdit(TextEdit e) {
+        checkEdit(e);
+
+        loc f = e.range.top;
+        if ([*pre, c:changed(f, _)] := docEdits) {
+            // If possible, merge with latest document edit
+            // TODO Just assign to docEdits[-1], once this issue has been solved:
+            // https://github.com/usethesource/rascal/issues/2123
+            docEdits = [*pre, c[edits = c.edits + e]];
+        } else {
+            // Else, create new document edit
+            docEdits += changed(f, [e]);
+        }
+    };
+
+    Renamer r = renamer(
+        registerMessage
+      , registerDocumentEdit
+      , registerTextEdit
+      , RenameConfig() { return cachedConfig; }
+      , void(value at, str s) { registerMessage(info(at, s)); }
+      , void(value at, str s) { registerMessage(warning(at, s)); }
+      , void(value at, str s) { registerMessage(error(at, s)); }
+    );
+
+    jobStep(config.jobLabel, "Resolving definitions of <cursor[0].src>", work = WORKSPACE_WORK);
+    defs = getCursorDefinitions(cursor, parseLocCached, getTModelCached, r);
+
+    if (defs == {}) r.error(cursor[0].src, "No definitions found");
+    if (errorReported()) {
+        jobEnd(config.jobLabel, success=false);
+        return <sortDocEdits(docEdits), getMessages()>;
+    }
+
+    jobStep(config.jobLabel, "Looking for files with occurrences of name under cursor", work = WORKSPACE_WORK);
+    <maybeDefFiles, maybeUseFiles, newNameFiles> = findOccurrenceFiles(defs, cursor, newName, parseLocCached, r);
+
+    jobTodo(config.jobLabel, work = (size(maybeDefFiles) + size(maybeUseFiles) + size(newNameFiles)) * FILE_WORK);
+
+    set[Define] additionalDefs = {};
+    solve (additionalDefs) {
+        for (loc f <- maybeDefFiles) {
+            jobStep(config.jobLabel, "Looking for additional definitions in <f>", work = 0);
+            tr = parseLocCached(f);
+            tm = getTModelCached(tr);
+            additionalDefs += findAdditionalDefinitions(defs, tr, tm, r);
+        }
+        defs += additionalDefs;
+    }
+    jobStep(config.jobLabel, "Done looking for additional definitions", work = FILE_WORK * size(maybeDefFiles));
+
+    for (loc f <- newNameFiles) {
+        jobStep(config.jobLabel, "Validating occurrences of new name \'<newName>\' in <f>", work = FILE_WORK);
+        tr = parseLocCached(f);
+        validateNewNameOccurrences(defs, newName, tr, r);
+    }
+    if (errorReported()) {
+        jobEnd(config.jobLabel, success = false);
+        return <sortDocEdits(docEdits), getMessages()>;
+    }
+
+    defFiles = {d.defined.top | d <- defs};
+    jobTodo(config.jobLabel, work = size(defFiles) * FILE_WORK);
+
+    for (loc f <- defFiles) {
+        fileDefs = {d | d <- defs, d.defined.top == f};
+        jobStep(config.jobLabel, "Renaming <size(fileDefs)> definitions in <f>", work = FILE_WORK);
+        tr = parseLocCached(f);
+        tm = getTModelCached(tr);
+
+        map[Define, loc] defNames = defNameLocations(tr, fileDefs, r);
+        for (d <- fileDefs) {
+            renameDefinition(d, defNames[d] ? d.defined, newName, tm, r);
+        }
+    }
+
+    if (errorReported()) {
+        jobEnd(config.jobLabel, success=false);
+        return <sortDocEdits(docEdits), getMessages()>;
+    }
+
+    for (loc f <- maybeUseFiles) {
+        jobStep(config.jobLabel, "Renaming uses in <f>", work = FILE_WORK);
+        tr = parseLocCached(f);
+        tm = getTModelCached(tr);
+
+        renameUses(defs, newName, tm, r);
+    }
+
+    set[Message] convertedMessages = getMessages();
+
+    if (config.debug) {
+        println("\n\n=================\nRename statistics\n=================\n");
+        int nDocs = size({f | de <- docEdits, f := (de has file ? de.file : de.from)});
+        int nEdits = (0 | it + ((changed(_, tes) := e) ? size(tes) : 1) | e <- docEdits);
+
+        int nErrors = size({msg | msg <- convertedMessages, msg is error});
+        int nWarnings = size({msg | msg <- convertedMessages, msg is warning});
+        int nInfos = size({msg | msg <- convertedMessages, msg is info});
+
+        println(" # of documents affected: <nDocs>");
+        println(" # of text edits:         <nEdits>");
+        println(" # of messages:           <size(convertedMessages)>");
+        println("   (<nErrors> errors, <nWarnings> warnings and <nInfos> infos)");
+
+        if (size(convertedMessages) > 0) {
+            println("\n===============\nMessages\n===============");
+            for (msg <- convertedMessages) {
+                println(" ** <msg>");
+            }
+            println();
+        }
+    }
+
+    jobEnd(config.jobLabel, success = !errorReported());
+    return <sortDocEdits(docEdits), convertedMessages>;
+}
+
+// Workaround to be able to pattern match on the emulated `src` field
+data Tree (loc src = |unknown:///|(0,0,<0,0>,<0,0>));
path: src/analysis/typepal/refactor/Rename.rsc
user: login: toinehartman
body: Vervangen door `@loc` waar nodig.
created_at: 2025-05-13T09:19:13Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,419 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module analysis::typepal::refactor::Rename
+
+import analysis::diff::edits::TextEdits;
+import analysis::typepal::FailMessage;
+import analysis::typepal::Messenger;
+import analysis::typepal::TModel;
+
+import util::Monitor;
+
+import Exception;
+import IO;
+import List;
+import Map;
+import Message;
+import Node;
+import ParseTree;
+import Relation;
+import Set;
+
+alias RenameResult = tuple[list[DocumentEdit], set[Message]];
+alias Focus = list[Tree];
+
+// This leaves some room for more fine-grained steps should the user want to monitor that
+private int WORKSPACE_WORK = 10;
+private int FILE_WORK = 5;
+
+data Renamer
+    = renamer(
+        void(FailMessage) msg
+      , void(DocumentEdit) documentEdit
+      , void(TextEdit) textEdit
+      , RenameConfig() getConfig
+
+      // Helpers
+      , void(value, str) warning
+      , void(value, str) info
+      , void(value, str) error
path: src/analysis/typepal/refactor/Rename.rsc
user: login: toinehartman
body: Removed them since the varargs are not possible here
created_at: 2025-05-13T09:37:37Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,419 @@
+@license{
+Copyright (c) 2024-2025, Swat.engineering
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice,
+this list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+this list of conditions and the following disclaimer in the documentation
+and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGE.
+}
+module analysis::typepal::refactor::Rename
+
+import analysis::diff::edits::TextEdits;
+import analysis::typepal::FailMessage;
+import analysis::typepal::Messenger;
+import analysis::typepal::TModel;
+
+import util::Monitor;
+
+import Exception;
+import IO;
+import List;
+import Map;
+import Message;
+import Node;
+import ParseTree;
+import Relation;
+import Set;
+
+alias RenameResult = tuple[list[DocumentEdit], set[Message]];
+alias Focus = list[Tree];
+
+// This leaves some room for more fine-grained steps should the user want to monitor that
+private int WORKSPACE_WORK = 10;
+private int FILE_WORK = 5;
+
+data Renamer
+    = renamer(
+        void(FailMessage) msg
+      , void(DocumentEdit) documentEdit
+      , void(TextEdit) textEdit
+      , RenameConfig() getConfig
+
+      // Helpers
+      , void(value, str) warning
+      , void(value, str) info
+      , void(value, str) error
path: src/analysis/typepal/refactor/Rename.rsc
user: login: PaulKlint
body: The arguments are flipped to achieve a printf style with a format string and a variable number of arguments at the end. But I agree, it is also a bit confusing.
created_at: 2025-05-13T21:50:24Z
author_association: MEMBER
diff_hunk: @@ -88,11 +88,15 @@ void collect(current: (Expression) `<Id name>`,  Collector c){
 void collect(current: (Expression) `<Expression exp1> (<Expression exp2>)`, Collector c) { 
      c.calculate("application", current, [exp1, exp2],
          AType (Solver s) {  
-            if(functionType(tau1, tau2) := s.getType(exp1)){
+            tp = s.getType(exp1);
+            if(functionType(tau1, tau2) := tp){
+                  s.requireEqual(exp2, tau1, error(exp2, "Incorrect type of actual parameter"));
+                  return tau2;
+               } else if (overloadedAType(types) := tp, {<_, functionType(tau1, tau2)>} := types<1, 2>) {
                   s.requireEqual(exp2, tau1, error(exp2, "Incorrect type of actual parameter"));
                   return tau2;
                } else {
-                  s.report(error(exp1, "Function type expected"));
+                  s.report(error(exp1, "Function type expected, got <s.getType(exp1)>"));
path: src/examples/fun/Checker.rsc
user: login: DavyLandman
body: nit: should use `%t", exp1` to let the framework do the formatting if needed.
created_at: 2025-05-15T13:53:16Z
author_association: MEMBER

PR comments:
user: login: toinehartman
created_at: 2025-05-12T11:17:36Z
body: This PR is ready for review. I will fix the merge conflict once `main` stabilizes.
author_association: MEMBER
user: login: DavyLandman
created_at: 2025-05-12T11:31:54Z
body: Since this is getting ported to typepal, we should consider aligning the `Messages` overload that exists on the collector & solver with the renamer. I'm talking about the templating etc.

Or do they differ too much in their use? I'll leave this for @PaulKlint 
author_association: MEMBER
user: login: toinehartman
created_at: 2025-05-12T12:07:05Z
body: @DavyLandman I think you're alluding to `FailMessage`, which this framework already uses. Only just before returning, these are [converted](https://github.com/usethesource/typepal/pull/14/files#diff-331b6ee7eb6cff5a20ed758324cb50852cd52a0ef2e6c39028bbee40a9bc6963R174) to regular stdlib `Message`. 
author_association: MEMBER