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;