java-topology/defects/godot/patch/godot-0009-font-is-cyclic-hashset.patch
russell@unturf.com 3986d8dc50 diamond hunt: godot-0009/0010 + meson-0002 + typeorm-0004/0005 + ts-0003; count 629→635
New diamond recursion defects (O(2^D) → O(N)):
- godot-0009: Font::_is_cyclic no visited set — CJK fallback diamond, 2648x at F=4,D=8
- godot-0010: Font::_update_rids_fb no visited set — duplicate RIDs + O(N^2) hot path
- meson-0002: get_internal_static_libraries_recurse link_whole guard missing — 132x at D=10
- typescript-0003: hasBaseType inner check() no visited set — 1024x at D=10; hot on instanceof

New O(N²) defects:
- typeorm-0004: SubjectTopologicalSorter Array.indexOf dedup — 200x at N=400
- typeorm-0005: DepGraph.createDFS result.indexOf + addDependency edge dedup — 300x at N=600

CLEAN confirmed (diamond recursion sweep): bazel, cargo, cmake, composer, dgl, diesel,
doctrine-orm, efcore, helm, mybatis, networkx-deeper, ninja, npm-arborist, peewee, pip,
rubygems, seaorm, sqlalchemy, swift

UNDF: 571→578 assigned; MOAD count: 629→635
2026-03-29 16:52:04 -04:00

72 lines
2.4 KiB
Diff

# UNDF: UNDF-2026-000000404
--- a/scene/resources/font.h
+++ b/scene/resources/font.h
@@ -31,6 +31,7 @@
#include "core/io/resource.h"
#include "core/templates/hash_map.h"
+#include "core/templates/hash_set.h"
#include "core/templates/list.h"
#include "core/variant/typed_array.h"
#include "servers/text_server.h"
@@ -96,7 +97,8 @@ class Font : public Resource {
static void _bind_methods();
virtual void _update_rids_fb(const Font *p_f, int p_depth) const;
- virtual void _update_rids() const;
+ virtual void _update_rids() const; // delegates to _update_rids_fb
virtual void reset_state() override;
public:
- virtual bool _is_cyclic(const Ref<Font> &p_f, int p_depth) const;
- virtual bool _is_base_cyclic(const Ref<Font> &p_f, int p_depth) const;
+ // FIX godot-0009: add visited HashSet to _is_cyclic to avoid O(F^D) re-traversal
+ virtual bool _is_cyclic(const Ref<Font> &p_f, int p_depth) const; // public wrapper
+ bool _is_cyclic_internal(const Ref<Font> &p_f, int p_depth, HashSet<const Font *> &r_visited) const;
+ virtual bool _is_base_cyclic(const Ref<Font> &p_f, int p_depth) const;
virtual void _invalidate_rids();
--- a/scene/resources/font.cpp
+++ b/scene/resources/font.cpp
@@ -134,16 +134,28 @@ void Font::_invalidate_rids() {
bool Font::_is_cyclic(const Ref<Font> &p_f, int p_depth) const {
- ERR_FAIL_COND_V(p_depth > MAX_FALLBACK_DEPTH, true);
- if (p_f.is_null()) {
- return false;
- }
- if (p_f == this) {
- return true;
- }
- for (int i = 0; i < p_f->fallbacks.size(); i++) {
- const Ref<Font> &f = p_f->fallbacks[i];
- if (_is_cyclic(f, p_depth + 1)) {
- return true;
- }
- }
- return false;
+ // FIX godot-0009: was O(F^D) — shared fonts in diamond fallback graph
+ // re-traversed exponentially; now O(N) with visited set.
+ HashSet<const Font *> visited;
+ return _is_cyclic_internal(p_f, p_depth, visited);
+}
+
+bool Font::_is_cyclic_internal(const Ref<Font> &p_f, int p_depth, HashSet<const Font *> &r_visited) const {
+ ERR_FAIL_COND_V(p_depth > MAX_FALLBACK_DEPTH, true);
+ if (p_f.is_null()) {
+ return false;
+ }
+ if (p_f == this) {
+ return true;
+ }
+ const Font *raw = p_f.ptr();
+ if (r_visited.has(raw)) {
+ return false; // already proven non-cyclic from this node; skip
+ }
+ r_visited.insert(raw);
+ for (int i = 0; i < p_f->fallbacks.size(); i++) {
+ const Ref<Font> &f = p_f->fallbacks[i];
+ if (_is_cyclic_internal(f, p_depth + 1, r_visited)) {
+ return true;
+ }
+ }
+ return false;
}