diff --git a/src/analysis/typepal/ConfigurableScopeGraph.rsc b/src/analysis/typepal/ConfigurableScopeGraph.rsc index d6df65c..d254918 100644 --- a/src/analysis/typepal/ConfigurableScopeGraph.rsc +++ b/src/analysis/typepal/ConfigurableScopeGraph.rsc @@ -75,6 +75,11 @@ bool defaultReportUnused (loc _, TModel _) { return false; } +list[loc] defaultFilterUnused(list[loc] defs, TModel tm) { + bool(loc, TModel) reportUnused = tm.config.reportUnused; + return [d | loc d <- defs, reportUnused(d, tm)]; +} + // https://en.wikipedia.org/wiki/Uniform_Resource_Identifier#:~:text=A%20URI%20is%20composed%20from,)%2C%20and%20the%20character%20%25%20. // gen-delims: : / ? # [ ] @ // sub-delims: ! $ & ' ( ) * + , ; @@ -157,12 +162,16 @@ data TypePalConfig( bool(loc def, TModel tm) reportUnused = defaultReportUnused, + list[loc](list[loc] defs, TModel tm) filterUnused = defaultFilterUnused, + loc (Define def, str modelName, PathConfig pcfg) createLogicalLoc = defaultLogicalLoc, list[str] (Use u, TModel tm) similarNames = defaultSimilarNames, bool enableErrorFixes = true, + bool enableSortedMessages = false, + int cutoffForNameSimilarity = 3 ); @@ -404,11 +413,31 @@ ScopeGraph newScopeGraph(TModel tm, TypePalConfig config){ /* parents) and definitions that can be reached in a single step via semantic links */ /************************************************************************************/ + // Convert `tm.definesMap` to a more efficient representation for the kind + // of lookups that are performed in `bindWide`. The idea is to convert only + // once, and enjoy a return on investment each time when a lookup is + // performed in it (instead of also needing a `domainR` call each time). + map[loc, map[str, map[IdRole, set[loc]]]] convertDefinesMap() { + // Conversion function for the outer map + map[loc, map[str, map[IdRole, set[loc]]]] convertOuter(map[loc, map[str, rel[IdRole, loc]]] scope2id2pairs) { + return (scope: convertInner(scope2id2pairs[scope]) | loc scope <- scope2id2pairs); + } + // Conversion function for the inner maps + map[str, map[IdRole, set[loc]]] convertInner(map[str, rel[IdRole, loc]] id2pairs) { + return (id: Relation::index(id2pairs[id]) | str id <- id2pairs); + } + return convertOuter(tm.definesMap); + } + + // Convert only once. (Note: this variable is local to `newScopeGraph`, so + // always associated with the same TModel.) + map[loc, map[str, map[IdRole, set[loc]]]] scope2id2role2defs = convertDefinesMap(); + //@memo // Retrieve all bindings for use in given syntactic scope private set[loc] bindWide(loc scope, str id, set[IdRole] idRoles){ - idsInScope = (scope in tm.definesMap) ? tm.definesMap[scope] : (); - foundDefs = id in idsInScope ? domainR(idsInScope[id], idRoles)<1> : {}; + map[IdRole, set[loc]] role2defs = (scope2id2role2defs[scope] ? ())[id] ? (); + foundDefs = {*(role2defs[role] ? {}) | IdRole role <- idRoles}; // dbg("bindWide: , =\> "); return foundDefs; } @@ -421,26 +450,37 @@ ScopeGraph newScopeGraph(TModel tm, TypePalConfig config){ return res; } + // Cache to store results of `getPathTargets`. The assumption is that + // semantic paths might change between calls, so the cache needs to be + // invalidated when `the_solver.getPathsByPathRole()` returns an updated + // value (relative to the previous call of `getPathTargets`). + map[PathRole, map[loc, set[loc]]] getPathTargetsCache = (); + + // Gets the target of each path with the provided role and source + set[loc] getPathTargets(PathRole role, loc source) { + if (role notin getPathTargetsCache) { + getPathTargetsCache[role] = (); + } + if (source notin getPathTargetsCache[role]) { + getPathTargetsCache[role][source] = {target | <- pathsByPathRole[role]}; + } + return getPathTargetsCache[role][source]; + } + //@memo // Find all (semantics induced, one-level) bindings for use in given syntactic scope via PathRole private set[loc] lookupPathsWide(loc scope, Use use, PathRole pathRole){ // dbgEnter("lookupPathsWide: in scope , role ");; res = {}; - - seenParents = {}; - solve(res, scope) { - next_path: - for( <- pathsByPathRole[pathRole] ? {}, parent notin seenParents){ - seenParents += parent; - for(loc def <- lookupScopeWide(parent, use)){ - switch(isAcceptablePathFun(parent, def, use, pathRole, the_solver)){ - case acceptBinding(): - res += def; - case ignoreContinue(): - continue; - case ignoreSkipPath(): - continue next_path; - } + for (loc parent <- getPathTargets(pathRole, scope)) { + for (loc def <- lookupScopeWide(parent, use)) { + switch (isAcceptablePathFun(parent, def, use, pathRole, the_solver)) { + case acceptBinding(): + res += def; + case ignoreContinue(): + continue; // Continue inner loop + case ignoreSkipPath(): + break; // Break inner loop (continue outer loop) } } } @@ -497,13 +537,25 @@ ScopeGraph newScopeGraph(TModel tm, TypePalConfig config){ return res; } - public set[loc] lookupWide(Use u){ + // Cache to store results of `lookupWide`. The assumption is that syntactic + // scopes will not change between calls, but semantic paths might, so the + // cache needs to be invalidated when `the_solver.getPathsByPathRole()` + // returns an updated value (relative to the previous call of `lookupWide`). + map[Use, set[loc]] lookupWideCache = (); + public set[loc] lookupWide(Use u){ // Update current paths and pathRoles current_pathsByPathRole = the_solver.getPathsByPathRole(); if(current_pathsByPathRole != pathsByPathRole){ pathsByPathRole = current_pathsByPathRole; pathRoles = domain(pathsByPathRole); + getPathTargetsCache = (); + lookupWideCache = (); + } + + if (u in lookupWideCache) { + set[loc] defs = lookupWideCache[u]; + if (isEmpty(defs)) throw NoBinding(); else return defs; } scope = u.scope; @@ -512,6 +564,7 @@ ScopeGraph newScopeGraph(TModel tm, TypePalConfig config){ // dbgPaths(); if(!(u has qualifierRoles)){ defs = {def | loc def <- lookupNestWide(scope, u), isAcceptableSimpleFun(def, u, the_solver) == acceptBinding()}; + lookupWideCache[u] = defs; // dbg("lookupWide: =\> "); if(isEmpty(defs)) throw NoBinding(); else return defs; } else { @@ -528,6 +581,7 @@ ScopeGraph newScopeGraph(TModel tm, TypePalConfig config){ scopeLookups = lookupNestWide(qscope, use(u.ids[-1], "", u.occ, qscope, u.idRoles)); defs += { def | def <- scopeLookups, isAcceptableQualifiedFun(def, u, the_solver) == acceptBinding()}; } + lookupWideCache[u] = defs; if(!isEmpty(defs)){ // dbg("lookupWide: returns:\n\t==\> <}>"); return defs; diff --git a/src/analysis/typepal/Solver.rsc b/src/analysis/typepal/Solver.rsc index 2a118e9..18d4e5a 100644 --- a/src/analysis/typepal/Solver.rsc +++ b/src/analysis/typepal/Solver.rsc @@ -141,7 +141,7 @@ Solver newSolver(map[str,Tree] namedTrees, TModel tm){ AType(AType containerType, Tree selector, loc scope, Solver s) getTypeInNamelessTypeFun = defaultGetTypeInNamelessType; - bool(loc def, TModel tm) reportUnused = defaultReportUnused; + list[loc](list[loc] defs, TModel tm) filterUnused = defaultFilterUnused; map[loc,loc] logical2physical = tm.logical2physical; @@ -177,7 +177,7 @@ Solver newSolver(map[str,Tree] namedTrees, TModel tm){ getTypeNamesAndRole = tc.getTypeNamesAndRole; getTypeInTypeFromDefineFun = tc.getTypeInTypeFromDefine; getTypeInNamelessTypeFun = tc.getTypeInNamelessType; - reportUnused = tc.reportUnused; + filterUnused = tc.filterUnused; } TypePalConfig solver_getConfig() = tm.config; @@ -1024,18 +1024,82 @@ Solver newSolver(map[str,Tree] namedTrees, TModel tm){ } newPaths = { tup | tup: <- newPaths, u != d }; tm.referPaths = referPaths; - newPathFound = !isEmpty(newPaths); - if( newPathFound // we found new paths - || (!isEmpty(tm.paths) && isEmpty(pathsByPathRole)) // pathsByPathRole not yet initialized - ){ - tm.paths += newPaths; - pathsByPathRole = (); - for( <- tm.paths){ - pathsByPathRole[r] ? {} += {}; - } + tm.paths += newPaths; + + initPathsByPathRole(); + updatePathsByPathRole(newPaths); + return !isEmpty(newPaths); + } + + void initPathsByPathRole() { + if (!isEmpty(pathsByPathRole)) { // Already initialized + return; } - return newPathFound; + paths = tm.paths; + if (isEmpty(paths)) { // Nothing to initialize + return; + } + + pathsByPathRole = (r: {} | <_, PathRole r, _> <- paths); + for (PathRole r <- pathsByPathRole) { + pathsByPathRole[r] = { | <- paths}; + } + // That is, first compute the keys, and second compute the total + // values. It seems to be significantly faster than computing keys + // and total values together: + // ``` + // pathsByPathRole = (r: { | <- paths} | <_, PathRole r, _> <- paths); + // ``` + // It also seems to be significantly faster than computing keys + // and partial values iteratively: + // ``` + // pathsByPathRole = (); + // for ( <- tm.paths) { + // pathsByPathRole[r] ? {} += {}; + // } + // ``` + } + + void updatePathsByPathRole(Paths newPaths) { + for( <- newPaths){ + pathsByPathRole[r] ? {} += {}; + } + } + + // ---- Illegal overloading ----------------------------------------------- + + void checkIllegalOverloadingOfUnusedDefinitions() { + map[str, set[Define]] definesById = Relation::index({ | Define d <- defines}); + for (str id <- definesById) { + set[Define] definesOfId = definesById[id]; + + // `id` can be illegally overloaded only if it isn't unique (i.e., + // it has at least two definitions). + for (size(definesOfId) >= 2, Define d <- definesOfId, !isUsed(d)) { + + // Turn unused definition into use and check for double + // declarations using scope graph lookup (i.e., not each + // definition in `definesOfId` might be in scope of `d`). + Use u = use(d.id, d.orgId, d.defined, d.scope, {d.idRole}); + try { + set[loc] foundDefs = scopeGraph.lookup(u); + if (size(foundDefs) > 1 && !mayOverloadFun(foundDefs, definitions)) { + doubleDefs += foundDefs; + messages += [error("Double declaration of ``", d1, + causes=[info("Other declaration of ``", d2) | d2 <- foundDefs, d2 != d1 ]) + | d1 <- foundDefs, isContainedIn(u.scope, definitions[d1].scope, logical2physical) + ]; + } + } + catch NoBinding(): {;} + catch TypeUnavailable(): {;} + } + } + } + + bool isUsed(Define d) { + return d.defined in def2uses; } // ---- "equal" and "requireEqual" ---------------------------------------- @@ -1467,41 +1531,7 @@ Solver newSolver(map[str,Tree] namedTrees, TModel tm){ } } - // Check for illegal overloading of unused definitions - set[loc] unusedDefs = domain(definitions) - actuallyUsedDefs; - - for(ud <- unusedDefs){ - udef = definitions[ud]; - - scope = udef.scope; - id = udef.id; - orgId = udef.orgId; - idRole = udef.idRole; - defined = udef.defined; - //if(defined in logical2physical) defined = logical2physical[defined]; - - u = use(id, orgId, defined, scope, {idRole}); // turn each unused definition into a use and check for double declarations; - try { - foundDefs = scopeGraph.lookup(u); - if(isEmpty(foundDefs)){ - ;//throw TypePalInternalError("No binding found while checking for double definitions"); - } else - if(size(foundDefs) == 1 || mayOverloadFun(foundDefs, definitions)){ - ; - } else { - doubleDefs += foundDefs; - messages += [error("Double declaration of ``", d1, - causes=[info("Other declaration of ``", d2) | d2 <- foundDefs, d2 != d1 ]) - | d1 <- foundDefs, isContainedIn(u.scope, definitions[d1].scope, logical2physical) - ]; - } - } - catch NoBinding(): { - ;//throw TypePalInternalError("No binding found while checking for double definitions"); - } - } - - unusedDefs = actuallyUsedDefs = {}; + checkIllegalOverloadingOfUnusedDefinitions(); // Process all defines (which may create new calculators/facts) @@ -1645,14 +1675,29 @@ Solver newSolver(map[str,Tree] namedTrees, TModel tm){ /****************** end of main solve loop *****************************/ + // Creating new `defType` values using the `defType` constructor takes + // significant interpreter time. To make it faster, the following map + // stores "prototypes" from which new `defType` values can be created. + map[AType, DefInfo] defInfoPrototypes = (); + + DefInfo newDefInfo(AType t, map[str, value] keywordParameters) { + DefInfo proto; + if (t in defInfoPrototypes) { + proto = defInfoPrototypes[t]; + } else { + proto = defType(t); + defInfoPrototypes[t] = proto; + } + return setKeywordParameters(proto, keywordParameters); // Create new value (i.e., leave `proto` unchanged) + } + // Eliminate all defTypeCalls before handing control to the postSolver for(loc l <- definitions){ Define def = definitions[l]; if(defTypeCall(_, AType(Solver s) getAType) := def.defInfo){ kwparams = getKeywordParameters(def.defInfo); try { - di = defType(getAType(thisSolver)); - def.defInfo = setKeywordParameters(di, kwparams); + def.defInfo = newDefInfo(getAType(thisSolver), kwparams); definitions[l] = def; } catch _: { // Guard against type incorrect defines, but record for now ; //println("Skipping (type-incorrect) def: \n"); @@ -1663,11 +1708,16 @@ Solver newSolver(map[str,Tree] namedTrees, TModel tm){ newDefines = for(def <- defines){ + // All `defTypeCall` values have already been eliminated from + // `definitions`, so this is a fast(er) way out. + if (def.defined in definitions) { + append definitions[def.defined]; + continue; + } if(defTypeCall(_, AType(Solver s) getAType) := def.defInfo){ kwparams = getKeywordParameters(def.defInfo); try { - di = defType(getAType(thisSolver)); - def.defInfo = setKeywordParameters(di, kwparams); + def.defInfo = newDefInfo(getAType(thisSolver), kwparams); } catch _: { // Guard against type incorrect defines, but record for now ; //println("Skipping (type-incorrect) def: \n"); } @@ -1805,16 +1855,14 @@ Solver newSolver(map[str,Tree] namedTrees, TModel tm){ ldefines = for(tup: <- tm.defines){ if(defInfo has tree){ l = getLogicalLoc(defInfo.tree); - if(l in tm.facts){ - dt = defType(tm.facts[l]); - tup.defInfo = setKeywordParameters(dt, getKeywordParameters(defInfo)); + if(l in facts){ + tup.defInfo = newDefInfo(facts[l], getKeywordParameters(defInfo)); } else { continue; } } else { - if(defined in tm.facts){ - dt = defType(tm.facts[defined]); - tup.defInfo = setKeywordParameters(dt, getKeywordParameters(defInfo)); + if(defined in facts){ + tup.defInfo = newDefInfo(facts[defined], getKeywordParameters(defInfo)); } else { continue; } @@ -1823,14 +1871,15 @@ Solver newSolver(map[str,Tree] namedTrees, TModel tm){ }; tm.defines = toSet(ldefines); - for(Define def <- tm.defines){ - defdefined = solver_toPhysicalLoc(def.defined); - if(defdefined notin def2uses && defdefined notin doubleDefs && reportUnused(defdefined, tm)){ - messages += warning("Unused ``", defdefined); - } - } + list[loc] unused = filterUnused([l | Define def <- tm.defines, loc l := solver_toPhysicalLoc(def.defined), l notin def2uses, l notin doubleDefs], tm); + messages += [warning("Unused ``", l) | loc l <- unused, Define def := definitions[l]]; + messages = visit(messages) { case loc l => solver_toPhysicalLoc(l) }; - tm.messages = sortMostPrecise(toList(toSet(messages))); + messages = toList(toSet(messages)); // Remove duplicates + if (tm.config.enableSortedMessages) { + messages = sortMostPrecise(messages); + } + tm.messages = messages; assertValidDefines(tm); assertValidUseDef(tm, thisSolver);