diff --git a/Content.Tests/DMProject/Tests/Statements/VarDecl/cant_override_final.dm b/Content.Tests/DMProject/Tests/Statements/VarDecl/cant_override_final.dm new file mode 100644 index 0000000000..1bc2612c38 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Statements/VarDecl/cant_override_final.dm @@ -0,0 +1,7 @@ +// COMPILE ERROR OD0407 + +/datum + var/final/foo = 1 + +/datum/a + foo = 2 // Can't override a final var \ No newline at end of file diff --git a/DMCompiler/Compiler/CompilerError.cs b/DMCompiler/Compiler/CompilerError.cs index b2752df1ce..dad0930c91 100644 --- a/DMCompiler/Compiler/CompilerError.cs +++ b/DMCompiler/Compiler/CompilerError.cs @@ -22,6 +22,7 @@ public enum WarningCode { ItemDoesntExist = 404, DanglingOverride = 405, StaticOverride = 406, + FinalOverride = 407, // ReSharper disable once InconsistentNaming IAmATeaPot = 418, // TODO: Implement the HTCPC protocol for OD HardConstContext = 500, diff --git a/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs b/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs index 3d556981fa..a0f4f9dd81 100644 --- a/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs +++ b/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs @@ -68,6 +68,7 @@ public sealed class DMASTObjectVarDefinition( public bool IsStatic => _varDecl.IsStatic; public bool IsConst => _varDecl.IsConst; + public bool IsFinal => _varDecl.IsFinal; public bool IsTmp => _varDecl.IsTmp; public readonly DMComplexValueType ValType = valType; diff --git a/DMCompiler/Compiler/DM/DMPath.cs b/DMCompiler/Compiler/DM/DMPath.cs index 752e050f54..d5e2265dd7 100644 --- a/DMCompiler/Compiler/DM/DMPath.cs +++ b/DMCompiler/Compiler/DM/DMPath.cs @@ -1,146 +1,157 @@ -namespace DMCompiler.Compiler.DM { - public abstract class VarDeclInfo { - public DreamPath? TypePath; - public string VarName; +namespace DMCompiler.Compiler.DM; - ///Marks whether the variable is /global/ or /static/. (These are seemingly interchangeable keywords in DM and so are under this same boolean) - public bool IsStatic; +internal abstract class VarDeclInfo { + public DreamPath? TypePath; + public string VarName; - public bool IsConst; - public bool IsList; - } + ///Marks whether the variable is /global/ or /static/. (These are seemingly interchangeable keywords in DM and so are under this same boolean) + public bool IsStatic; - public sealed class ProcVarDeclInfo : VarDeclInfo - { - public ProcVarDeclInfo(DreamPath path) - { - string[] elements = path.Elements; - var readIdx = 0; - List currentPath = new(); - if (elements[readIdx] == "var") - { - readIdx++; - } - while (readIdx < elements.Length - 1) - { - var elem = elements[readIdx]; - if (elem == "static" || elem == "global") - { + public bool IsConst; + public bool IsFinal; + public bool IsList; +} + +internal sealed class ProcVarDeclInfo : VarDeclInfo { + public ProcVarDeclInfo(DreamPath path) { + string[] elements = path.Elements; + var readIdx = 0; + List currentPath = new(); + if (elements[readIdx] == "var") { + readIdx++; + } + + while (readIdx < elements.Length - 1) { + var elem = elements[readIdx]; + switch (elem) { + case "static": + case "global": IsStatic = true; - } - else if (elem == "const") - { + break; + case "const": IsConst = true; - } - else if (elem == "list") - { + break; + case "final": + IsFinal = true; + break; + case "list": IsList = true; - } - else - { + break; + default: currentPath.Add(elem); - } - readIdx += 1; - } - if (currentPath.Count > 0) - { - TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); - } - else - { - TypePath = null; + break; } - VarName = elements[elements.Length - 1]; + + readIdx += 1; } + + if (currentPath.Count > 0) { + TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); + } else { + TypePath = null; + } + + VarName = elements[^1]; } +} - public sealed class ObjVarDeclInfo : VarDeclInfo - { - public DreamPath ObjectPath; - public bool IsTmp; - - public ObjVarDeclInfo(DreamPath path) - { - string[] elements = path.Elements; - var readIdx = 0; - List currentPath = new(); - while (readIdx < elements.Length && elements[readIdx] != "var") - { - currentPath.Add(elements[readIdx]); - readIdx += 1; - } - ObjectPath = new DreamPath(path.Type, currentPath.ToArray()); - if (ObjectPath.Elements.Length == 0) // Variables declared in the root scope are inherently static. - { - IsStatic = true; - } - currentPath.Clear(); +internal sealed class ObjVarDeclInfo : VarDeclInfo { + public DreamPath ObjectPath; + public readonly bool IsTmp; + + public ObjVarDeclInfo(DreamPath path) { + string[] elements = path.Elements; + var readIdx = 0; + List currentPath = new(); + while (readIdx < elements.Length && elements[readIdx] != "var") { + currentPath.Add(elements[readIdx]); readIdx += 1; - while (readIdx < elements.Length - 1) - { - var elem = elements[readIdx]; - if (elem == "static" || elem == "global") - { + } + + ObjectPath = new DreamPath(path.Type, currentPath.ToArray()); + if (ObjectPath.Elements.Length == 0) { // Variables declared in the root scope are inherently static. + IsStatic = true; + } + + currentPath.Clear(); + readIdx += 1; + while (readIdx < elements.Length - 1) { + var elem = elements[readIdx]; + switch (elem) { + case "static": + case "global": IsStatic = true; - } - else if (elem == "const") - { + break; + case "const": IsConst = true; - } - else if (elem == "list") - { + break; + case "final": + IsFinal = true; + break; + case "list": IsList = true; - } - else if (elem == "tmp") - { + break; + case "tmp": IsTmp = true; - } - else - { + break; + default: currentPath.Add(elem); - } - readIdx += 1; - } - if (currentPath.Count > 0) - { - TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); + break; } - else - { - TypePath = null; - } - VarName = elements[elements.Length - 1]; + + readIdx += 1; + } + + if (currentPath.Count > 0) { + TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); + } else { + TypePath = null; } + + VarName = elements[^1]; } +} - public sealed class ProcParameterDeclInfo : VarDeclInfo { - public ProcParameterDeclInfo(DreamPath path) { - string[] elements = path.Elements; - var readIdx = 0; - List currentPath = new(); - if (elements[readIdx] == "var") { - readIdx++; - } - while (readIdx < elements.Length - 1) { - var elem = elements[readIdx]; - if (elem == "static" || elem == "global") { +internal sealed class ProcParameterDeclInfo : VarDeclInfo { + public ProcParameterDeclInfo(DreamPath path) { + string[] elements = path.Elements; + var readIdx = 0; + List currentPath = new(); + if (elements[readIdx] == "var") { + readIdx++; + } + + while (readIdx < elements.Length - 1) { + var elem = elements[readIdx]; + switch (elem) { + case "static": + case "global": //No effect - } else if (elem == "const") { + break; + case "const": //TODO: Parameters can be constant //If they are they can't be assigned to but still cannot be used in const-only contexts (such as switch cases) - } else if (elem == "list") { + break; + case "final": + IsFinal = true; + break; + case "list": IsList = true; - } else { + break; + default: currentPath.Add(elem); - } - readIdx += 1; + break; } - if (currentPath.Count > 0) { - TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); - } else { - TypePath = null; - } - VarName = elements[elements.Length - 1]; + + readIdx += 1; + } + + if (currentPath.Count > 0) { + TypePath = new DreamPath(DreamPath.PathType.Absolute, currentPath.ToArray()); + } else { + TypePath = null; } + + VarName = elements[^1]; } } diff --git a/DMCompiler/DM/DMCodeTree.Vars.cs b/DMCompiler/DM/DMCodeTree.Vars.cs index 3c399703de..667b96cbfd 100644 --- a/DMCompiler/DM/DMCodeTree.Vars.cs +++ b/DMCompiler/DM/DMCodeTree.Vars.cs @@ -125,7 +125,7 @@ private bool HandleGlobalVar(DMCompiler compiler, DMObject dmObject, int pass) { return false; int globalId = compiler.DMObjectTree.CreateGlobal(out DMVariable global, varDef.Type, VarName, varDef.IsConst, - varDef.ValType); + varDef.IsFinal, varDef.ValType); dmObject.AddGlobalVariable(global, globalId); _defined = true; @@ -152,7 +152,7 @@ private bool HandleInstanceVar(DMCompiler compiler, DMObject dmObject) { if (!TryBuildValue(new(compiler, dmObject, null), varDef.Value, varDef.Type, ScopeMode.Normal, out var value)) return false; - var variable = new DMVariable(varDef.Type, VarName, false, varDef.IsConst, varDef.IsTmp, varDef.ValType); + var variable = new DMVariable(varDef.Type, VarName, false, varDef.IsConst, varDef.IsFinal, varDef.IsTmp, varDef.ValType); dmObject.AddVariable(variable); _defined = true; @@ -222,6 +222,11 @@ public override bool TryDefineVar(DMCompiler compiler, int pass) { $"Var \"{VarName}\" is const and cannot be modified"); _finished = true; return true; + } else if (variable.IsFinal) { + compiler.Emit(WarningCode.FinalOverride, varOverride.Location, + $"Var \"{VarName}\" is final and cannot be modified"); + _finished = true; + return true; } else if (variable.ValType.IsCompileTimeReadOnly) { compiler.Emit(WarningCode.WriteToConstant, varOverride.Location, $"Var \"{VarName}\" is a native read-only value which cannot be modified"); @@ -267,7 +272,7 @@ public override bool TryDefineVar(DMCompiler compiler, int pass) { } int globalId = compiler.DMObjectTree.CreateGlobal(out DMVariable global, varDecl.Type, varDecl.Name, varDecl.IsConst, - varDecl.ValType); + false, varDecl.ValType); global.Value = new Null(Location.Internal); proc.AddGlobalVariable(global, globalId); diff --git a/DMCompiler/DM/DMObjectTree.cs b/DMCompiler/DM/DMObjectTree.cs index 3dad425614..fe25b20d74 100644 --- a/DMCompiler/DM/DMObjectTree.cs +++ b/DMCompiler/DM/DMObjectTree.cs @@ -162,10 +162,10 @@ public bool TryGetTypeId(DreamPath path, out int typeId) { return null; } - public int CreateGlobal(out DMVariable global, DreamPath? type, string name, bool isConst, DMComplexValueType valType) { + public int CreateGlobal(out DMVariable global, DreamPath? type, string name, bool isConst, bool isFinal, DMComplexValueType valType) { int id = Globals.Count; - global = new DMVariable(type, name, true, isConst, false, valType); + global = new DMVariable(type, name, true, isConst, isFinal, false, valType); Globals.Add(global); return id; } diff --git a/DMCompiler/DM/DMVariable.cs b/DMCompiler/DM/DMVariable.cs index 2ff3b649e8..dc5cd772cc 100644 --- a/DMCompiler/DM/DMVariable.cs +++ b/DMCompiler/DM/DMVariable.cs @@ -4,24 +4,27 @@ namespace DMCompiler.DM; internal sealed class DMVariable { public DreamPath? Type; - public string Name; - public bool IsGlobal; + public readonly string Name; + public readonly bool IsGlobal; + public readonly bool IsTmp; + public readonly bool IsFinal; + public DMExpression? Value; + public DMComplexValueType ValType; + /// /// NOTE: This DMVariable may be forced constant through opendream_compiletimereadonly. This only marks that the variable has the DM quality of /const/ness. /// - public bool IsConst; - public bool IsTmp; - public DMExpression? Value; - public DMComplexValueType ValType; + public readonly bool IsConst; public bool CanConstFold => (IsConst || ValType.Type.HasFlag(DMValueType.CompiletimeReadonly)) && !ValType.Type.HasFlag(DMValueType.NoConstFold); - public DMVariable(DreamPath? type, string name, bool isGlobal, bool isConst, bool isTmp, DMComplexValueType? valType = null) { + public DMVariable(DreamPath? type, string name, bool isGlobal, bool isConst, bool isFinal, bool isTmp, DMComplexValueType? valType = null) { Type = type; Name = name; IsGlobal = isGlobal; IsConst = isConst; + IsFinal = isFinal; IsTmp = isTmp; Value = null; ValType = valType ?? DMValueType.Anything; @@ -32,6 +35,7 @@ public DMVariable(DMVariable copyFrom) { Name = copyFrom.Name; IsGlobal = copyFrom.IsGlobal; IsConst = copyFrom.IsConst; + IsFinal = copyFrom.IsFinal; IsTmp = copyFrom.IsTmp; Value = copyFrom.Value; ValType = copyFrom.ValType;