Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/ZeroC.Slice.Symbols/BasicEnum.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ public abstract class BasicEnum : Entity, ISymbol, IType

/// <summary>Finds an enumerator by its Slice identifier.</summary>
public abstract Entity? FindEnumeratorByIdentifier(string identifier);

/// <summary>Gets the enumerators as a sequence of entities (without their typed values).</summary>
internal abstract IEnumerable<Entity> EnumeratorEntities { get; }
}

/// <summary>Represents a Slice basic enumeration with a typed underlying value.</summary>
Expand All @@ -27,6 +30,8 @@ public sealed class BasicEnum<T> : BasicEnum where T : struct, System.Numerics.I
public override Entity? FindEnumeratorByIdentifier(string identifier) =>
Enumerators.FirstOrDefault(e => e.Identifier == identifier);

internal override IEnumerable<Entity> EnumeratorEntities => Enumerators;

/// <summary>Represents an enumerator in a Slice basic enumeration.</summary>
public sealed class Enumerator : Entity
{
Expand Down
2 changes: 1 addition & 1 deletion src/ZeroC.Slice.Symbols/Entity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ public class Entity
public Entity? Parent { get; internal set; }

/// <summary>Gets the doc comment associated with this entity, if any.</summary>
public Comment? Comment { get; init; }
public Comment? Comment { get; internal set; }

/// <summary>Gets the fully scoped Slice identifier (e.g. "MyModule::MyType" or
/// "MyModule::MyInterface::MyOperation").</summary>
Expand Down
166 changes: 124 additions & 42 deletions src/ZeroC.Slice.Symbols/SymbolConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ internal static ImmutableList<SliceFile> ConvertFiles(
IEnumerable<Compiler.SliceFile> referenceFiles)
{
var converter = new SymbolConverter(sourceFiles.Concat(referenceFiles));
return sourceFiles.Select(converter.ConvertFile).ToImmutableList();
return [.. sourceFiles.Select(converter.ConvertFile)];
}

private static readonly Dictionary<string, Builtin> _builtins = new(StringComparer.Ordinal)
Expand Down Expand Up @@ -60,21 +60,33 @@ private SymbolConverter(IEnumerable<Compiler.SliceFile> allFiles)
}
}

// Convert all named symbols in dependency order to fully populate _cache.
// Phase 1: convert all named symbols in dependency order. Doc-comment links are recorded as
// UnresolvedCommentLink and resolved in phase 2 — eager resolution here would miss any reference
// that targets a symbol not yet in _cache (forward references, self-references, or cycles between
// entities that aren't related by type dependencies).
foreach (string typeId in TopologicalSort())
{
(Compiler.SliceFile file, Compiler.Symbol symbol) = _named[typeId];
Module module = ConvertModule(file.ModuleDeclaration);
_cache[typeId] = ConvertSymbol(symbol, file, module);
}

// Phase 2: resolve doc-comment links now that the symbol table is fully populated.
foreach (ISymbol symbol in _cache.Values)
{
if (symbol is Entity entity)
{
ResolveEntityComments(entity);
}
}
Comment on lines +63 to +81
Copy link

Copilot AI Apr 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new two-phase doc-comment link resolution logic (record unresolved links during conversion, then resolve after the symbol table is populated) doesn’t appear to have any regression tests that validate resolved vs unresolved links (e.g., forward/self/cyclic links, and links to sub-entities like operations/fields/enumerators). Consider adding a generator/symbols test that asserts these links become ResolvedCommentLink where appropriate so future refactors don’t silently revert to unresolved links.

Copilot uses AI. Check for mistakes.
}

private SliceFile ConvertFile(Compiler.SliceFile file)
{
string moduleScope = file.ModuleDeclaration.Identifier;
Module module = ConvertModule(file.ModuleDeclaration);

var contents = ImmutableList.CreateBuilder<ISymbol>();
ImmutableList<ISymbol>.Builder contents = ImmutableList.CreateBuilder<ISymbol>();
for (int i = 0; i < file.Contents.Count; i++)
{
Compiler.Symbol raw = file.Contents[i];
Expand Down Expand Up @@ -144,7 +156,7 @@ private List<string> TopologicalSort()
foreach ((string typeId, (Compiler.SliceFile file, Compiler.Symbol symbol)) in _named)
{
var deps = new HashSet<string>(StringComparer.Ordinal);
CollectNamedTypeIds(symbol, file, deps);
CollectDependencies(symbol, file, deps);
pending[typeId] = deps;
}

Expand Down Expand Up @@ -180,7 +192,7 @@ private List<string> TopologicalSort()

return sorted;

void CollectNamedTypeIds(Compiler.Symbol symbol, Compiler.SliceFile file, HashSet<string> result)
void CollectDependencies(Compiler.Symbol symbol, Compiler.SliceFile file, HashSet<string> result)
{
IEnumerable<string> typeIds = symbol switch
{
Expand All @@ -204,7 +216,7 @@ void CollectNamedTypeIds(Compiler.Symbol symbol, Compiler.SliceFile file, HashSe
else if (int.TryParse(typeId, out int index))
{
// Anonymous type — recurse to collect any named types it references.
CollectNamedTypeIds(file.Contents[index], file, result);
CollectDependencies(file.Contents[index], file, result);
}
// else: builtin, ignore.
}
Expand Down Expand Up @@ -310,7 +322,7 @@ private Struct ConvertStruct(Compiler.Struct raw, Compiler.SliceFile file, Modul
return result;
}

private ISymbol ConvertBasicEnum(Compiler.BasicEnum raw, Module module)
private static ISymbol ConvertBasicEnum(Compiler.BasicEnum raw, Module module)
{
Builtin builtin = _builtins[raw.Underlying];
return builtin.Kind switch
Expand Down Expand Up @@ -360,8 +372,7 @@ private ISymbol ConvertBasicEnum(Compiler.BasicEnum raw, Module module)
module,
builtin,
(abs, _) => abs),
_ => throw new InvalidOperationException(
$"Unsupported enum underlying type: {builtin.Kind}"),
_ => throw new InvalidOperationException($"Unsupported enum underlying type: {builtin.Kind}"),
};
}

Expand All @@ -375,15 +386,17 @@ private ISymbol ConvertVariantEnum(Compiler.VariantEnum raw, Compiler.SliceFile
Module = module,
IsCompact = raw.IsCompact,
IsUnchecked = raw.IsUnchecked,
Variants = raw.Variants.Select(v => new VariantEnum.Variant
{
Identifier = v.EntityInfo.Identifier,
Attributes = ConvertAttributes(v.EntityInfo.Attributes),
Comment = ConvertComment(v.EntityInfo.Comment),
Module = module,
Discriminant = v.Discriminant,
Fields = v.Fields.Select(f => ConvertField(f, file, module)).ToImmutableList(),
}).ToImmutableList(),
Variants = [
.. raw.Variants.Select(v => new VariantEnum.Variant
{
Identifier = v.EntityInfo.Identifier,
Attributes = ConvertAttributes(v.EntityInfo.Attributes),
Comment = ConvertComment(v.EntityInfo.Comment),
Module = module,
Discriminant = v.Discriminant,
Fields = v.Fields.Select(f => ConvertField(f, file, module)).ToImmutableList(),
})
],
};
SetParent(result, result.Variants);
foreach (VariantEnum.Variant variant in result.Variants)
Expand All @@ -401,10 +414,7 @@ private Interface ConvertInterface(Compiler.Interface raw, Compiler.SliceFile fi
Attributes = ConvertAttributes(raw.EntityInfo.Attributes),
Comment = ConvertComment(raw.EntityInfo.Comment),
Module = module,
Bases = raw.Bases
.Select(baseId => ResolveNamedSymbol(baseId))
.OfType<Interface>()
.ToImmutableList(),
Bases = [.. raw.Bases.Select(ResolveNamedSymbol).OfType<Interface>()],
Operations = raw.Operations.Select(op => ConvertOperation(op, file, module)).ToImmutableList(),
};
SetParent(result, result.Operations);
Expand Down Expand Up @@ -433,7 +443,7 @@ private Operation ConvertOperation(
return result;
}

private BasicEnum<T> CreateBasicEnum<T>(
private static BasicEnum<T> CreateBasicEnum<T>(
Compiler.BasicEnum raw,
Module module,
Builtin builtin,
Expand All @@ -447,14 +457,16 @@ private BasicEnum<T> CreateBasicEnum<T>(
Module = module,
IsUnchecked = raw.IsUnchecked,
Underlying = builtin,
Enumerators = raw.Enumerators.Select(e => new BasicEnum<T>.Enumerator
{
Identifier = e.EntityInfo.Identifier,
Attributes = ConvertAttributes(e.EntityInfo.Attributes),
Comment = ConvertComment(e.EntityInfo.Comment),
Module = module,
Value = toValue(e.AbsoluteValue, e.HasNegativeValue),
}).ToImmutableList(),
Enumerators = [
.. raw.Enumerators.Select(e => new BasicEnum<T>.Enumerator
{
Identifier = e.EntityInfo.Identifier,
Attributes = ConvertAttributes(e.EntityInfo.Attributes),
Comment = ConvertComment(e.EntityInfo.Comment),
Module = module,
Value = toValue(e.AbsoluteValue, e.HasNegativeValue),
})
],
};
SetParent(result, result.Enumerators);
return result;
Expand Down Expand Up @@ -492,13 +504,15 @@ private static void SetParent(Entity parent, IEnumerable<Entity> children)
}

private static ImmutableList<Attribute> ConvertAttributes(IList<Compiler.Attribute> raw) =>
raw.Select(a => new Attribute
{
Directive = a.Directive,
Args = [.. a.Args],
}).ToImmutableList();
[
.. raw.Select(a => new Attribute
{
Directive = a.Directive,
Args = [.. a.Args],
})
];

private Comment? ConvertComment(Compiler.DocComment? raw)
private static Comment? ConvertComment(Compiler.DocComment? raw)
{
if (raw is null)
{
Expand All @@ -508,18 +522,86 @@ private static ImmutableList<Attribute> ConvertAttributes(IList<Compiler.Attribu
var overview = raw.Value.Overview.Select<Compiler.MessageComponent, CommentMessageComponent>(c => c switch
{
Compiler.MessageComponent.Text t => new CommentText(t.V),
Compiler.MessageComponent.Link l => new CommentInlineLink(ResolveLink(l.V)),
Compiler.MessageComponent.Link l => new CommentInlineLink(new UnresolvedCommentLink(l.V)),
_ => throw new InvalidOperationException($"Unknown MessageComponent kind: {c.GetType().FullName}")
}).ToImmutableList();

var seeTags = raw.Value.SeeTags.Select(ResolveLink).ToImmutableList();
var seeTags = raw.Value.SeeTags
.Select(id => (CommentLink)new UnresolvedCommentLink(id))
.ToImmutableList();

return new Comment { Overview = overview, SeeTags = seeTags };
}

private void ResolveEntityComments(Entity entity)
{
if (entity.Comment is Comment comment)
{
entity.Comment = ResolveComment(comment);
}

switch (entity)
{
case Interface i:
foreach (Operation op in i.Operations)
{
ResolveEntityComments(op);
}
break;
case Operation op:
foreach (Field p in op.Parameters)
{
ResolveEntityComments(p);
}
foreach (Field r in op.ReturnType)
{
ResolveEntityComments(r);
}
break;
case Struct s:
foreach (Field f in s.Fields)
{
ResolveEntityComments(f);
}
break;
case VariantEnum v:
foreach (VariantEnum.Variant variant in v.Variants)
{
ResolveEntityComments(variant);
}
break;
case VariantEnum.Variant variant:
foreach (Field f in variant.Fields)
{
ResolveEntityComments(f);
}
break;
case BasicEnum basicEnum:
foreach (Entity enumerator in basicEnum.EnumeratorEntities)
{
ResolveEntityComments(enumerator);
}
break;
}
}

private Comment ResolveComment(Comment comment)
{
var overview = comment.Overview.Select(c => c switch
{
CommentInlineLink l => new CommentInlineLink(ResolveOrKeep(l.Target)),
_ => c,
})
.ToImmutableList();

var seeTags = comment.SeeTags.Select(ResolveOrKeep).ToImmutableList();

return new Comment { Overview = overview, SeeTags = seeTags };

CommentLink ResolveLink(string entityId) =>
ResolveEntityById(entityId) is Entity entity
CommentLink ResolveOrKeep(CommentLink link) =>
link is UnresolvedCommentLink unresolved && ResolveEntityById(unresolved.Identifier) is Entity entity
? new ResolvedCommentLink(entity)
: new UnresolvedCommentLink(entityId);
: link;
}

private Entity? ResolveEntityById(string entityId)
Expand Down
Loading