diff --git a/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs b/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs
index 6c6aa57898..ee1b360563 100644
--- a/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs
+++ b/DMCompiler/Compiler/DM/AST/DMAST.ObjectStatements.cs
@@ -52,7 +52,7 @@ public sealed class DMASTObjectVarDefinition(
Location location,
DreamPath path,
DMASTExpression value,
- DMComplexValueType valType,
+ DMComplexValueType? valType,
DreamPath? valPath = null) : DMASTStatement(location) {
/// The path of the object that we are a property of.
public DreamPath ObjectPath => _varDecl.ObjectPath;
@@ -73,7 +73,7 @@ public sealed class DMASTObjectVarDefinition(
public bool IsConst => _varDecl.IsConst;
public bool IsTmp => _varDecl.IsTmp;
- public readonly DMComplexValueType ValType = valType;
+ public readonly DMComplexValueType? ValType = valType;
}
public sealed class DMASTMultipleObjectVarDefinitions(Location location, DMASTObjectVarDefinition[] varDefinitions)
diff --git a/DMCompiler/Compiler/DM/AST/DMAST.ProcStatements.cs b/DMCompiler/Compiler/DM/AST/DMAST.ProcStatements.cs
index c255f68fa2..c7fb86b0f9 100644
--- a/DMCompiler/Compiler/DM/AST/DMAST.ProcStatements.cs
+++ b/DMCompiler/Compiler/DM/AST/DMAST.ProcStatements.cs
@@ -29,13 +29,14 @@ public sealed class DMASTProcStatementExpression(Location location, DMASTExpress
public DMASTExpression Expression = expression;
}
-public sealed class DMASTProcStatementVarDeclaration(Location location, DMASTPath path, DMASTExpression? value, DMComplexValueType valType)
+public sealed class DMASTProcStatementVarDeclaration(Location location, DMASTPath path, DMASTExpression? value, DMComplexValueType? valType)
: DMASTProcStatement(location) {
public DMASTExpression? Value = value;
public DreamPath? Type => _varDecl.IsList ? DreamPath.List : _varDecl.TypePath;
+ public DMComplexValueType? ExplicitValType => valType;
- public DMComplexValueType ValType => valType;
+ public DMComplexValueType ValType => ExplicitValType ?? DMValueType.Anything;
public string Name => _varDecl.VarName;
public bool IsGlobal => _varDecl.IsStatic;
diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs
index bf7e025353..65ad019b9e 100644
--- a/DMCompiler/Compiler/DM/DMParser.cs
+++ b/DMCompiler/Compiler/DM/DMParser.cs
@@ -237,9 +237,9 @@ public DMASTFile File() {
Whitespace();
// Proc return type
- var types = AsComplexTypes();
+ var types = AsComplexTypes(parameters);
- DMASTProcBlockInner? procBlock = ProcBlock();
+ DMASTProcBlockInner? procBlock = ProcBlock(parameters);
if (procBlock is null) {
DMASTProcStatement? procStatement = ProcStatement();
@@ -300,7 +300,7 @@ public DMASTFile File() {
value = new DMASTConstantNull(loc);
}
- var valType = AsComplexTypes() ?? DMValueType.Anything;
+ var valType = AsComplexTypes();
var varDef = new DMASTObjectVarDefinition(loc, varPath, value, valType);
varDefinitions.Add(varDef);
@@ -539,12 +539,12 @@ public DMASTFile File() {
return null;
}
- private DMASTProcBlockInner? ProcBlock() {
+ private DMASTProcBlockInner? ProcBlock(List? procParameters = null) {
Token beforeBlockToken = Current();
bool hasNewline = Newline();
- DMASTProcBlockInner? procBlock = BracedProcBlock();
- procBlock ??= IndentedProcBlock();
+ DMASTProcBlockInner? procBlock = BracedProcBlock(procParameters);
+ procBlock ??= IndentedProcBlock(procParameters);
if (procBlock == null && hasNewline) {
ReuseToken(beforeBlockToken);
@@ -553,7 +553,7 @@ public DMASTFile File() {
return procBlock;
}
- private DMASTProcBlockInner? BracedProcBlock() {
+ private DMASTProcBlockInner? BracedProcBlock(List? procParameters = null) {
var loc = Current().Location;
if (Check(TokenType.DM_LeftCurlyBracket)) {
DMASTProcBlockInner? block;
@@ -561,7 +561,7 @@ public DMASTFile File() {
Whitespace();
Newline();
if (Current().Type == TokenType.DM_Indent) {
- block = IndentedProcBlock();
+ block = IndentedProcBlock(procParameters);
Newline();
Consume(TokenType.DM_RightCurlyBracket, "Expected '}'");
} else {
@@ -593,14 +593,14 @@ public DMASTFile File() {
return null;
}
- private DMASTProcBlockInner? IndentedProcBlock() {
+ private DMASTProcBlockInner? IndentedProcBlock(List? procParameters = null) {
var loc = Current().Location;
if (Check(TokenType.DM_Indent)) {
List statements = new();
List setStatements = new(); // set statements are weird and must be held separately.
do {
- (List? statements, List? setStatements) blockInner = ProcBlockInner();
+ (List? statements, List? setStatements) blockInner = ProcBlockInner(procParameters);
if (blockInner.statements is not null)
statements.AddRange(blockInner.statements);
if (blockInner.setStatements is not null)
@@ -621,14 +621,14 @@ public DMASTFile File() {
return null;
}
- private (List?, List?) ProcBlockInner() {
+ private (List?, List?) ProcBlockInner(List? procParameters = null) {
List procStatements = new();
List setStatements = new(); // We have to store them separately because they're evaluated first
DMASTProcStatement? statement;
do {
Whitespace();
- statement = ProcStatement();
+ statement = ProcStatement(procParameters);
if (statement is not null) {
Whitespace();
@@ -643,7 +643,7 @@ public DMASTFile File() {
return (procStatements.Count > 0 ? procStatements : null, setStatements.Count > 0 ? setStatements : null);
}
- private DMASTProcStatement? ProcStatement() {
+ private DMASTProcStatement? ProcStatement(List? procParameters = null) {
var loc = Current().Location;
if (Current().Type == TokenType.DM_Semicolon) { // A lone semicolon creates a "null statement" (like C)
@@ -748,20 +748,20 @@ public DMASTFile File() {
return new DMASTProcStatementExpression(loc, expression);
} else {
// These are sorted by frequency
- DMASTProcStatement? procStatement = If();
+ DMASTProcStatement? procStatement = If(procParameters);
procStatement ??= Return();
- procStatement ??= ProcVarDeclaration();
- procStatement ??= For();
+ procStatement ??= ProcVarDeclaration(procParameters);
+ procStatement ??= For(procParameters);
procStatement ??= Set();
- procStatement ??= Switch();
+ procStatement ??= Switch(procParameters);
procStatement ??= Continue();
procStatement ??= Break();
- procStatement ??= Spawn();
- procStatement ??= While();
- procStatement ??= DoWhile();
+ procStatement ??= Spawn(procParameters);
+ procStatement ??= While(procParameters);
+ procStatement ??= DoWhile(procParameters);
procStatement ??= Throw();
procStatement ??= Del();
- procStatement ??= TryCatch();
+ procStatement ??= TryCatch(procParameters);
procStatement ??= Goto();
if (procStatement != null) {
@@ -772,7 +772,7 @@ public DMASTFile File() {
}
}
- private DMASTProcStatement? ProcVarDeclaration(bool allowMultiple = true) {
+ private DMASTProcStatement? ProcVarDeclaration(List? procParameters = null, bool allowMultiple = true) {
Token firstToken = Current();
bool wasSlash = Check(TokenType.DM_Slash);
@@ -783,7 +783,7 @@ public DMASTFile File() {
}
Whitespace(); // We have to consume whitespace here since "var foo = 1" (for example) is valid DM code.
- DMASTProcStatementVarDeclaration[]? vars = ProcVarEnd(allowMultiple);
+ DMASTProcStatementVarDeclaration[]? vars = ProcVarEnd(procParameters, allowMultiple);
if (vars == null) {
Emit(WarningCode.InvalidVarDefinition, "Expected a var declaration");
return new DMASTInvalidProcStatement(firstToken.Location);
@@ -802,7 +802,7 @@ public DMASTFile File() {
///
/// This proc calls itself recursively.
///
- private DMASTProcStatementVarDeclaration[]? ProcVarBlock(DMASTPath? varPath) {
+ private DMASTProcStatementVarDeclaration[]? ProcVarBlock(List? procParameters, DMASTPath? varPath) {
Token newlineToken = Current();
bool hasNewline = Newline();
@@ -810,7 +810,7 @@ public DMASTFile File() {
List varDeclarations = new();
while (!Check(TokenType.DM_Dedent)) {
- DMASTProcStatementVarDeclaration[]? varDecl = ProcVarEnd(true, path: varPath);
+ DMASTProcStatementVarDeclaration[]? varDecl = ProcVarEnd(procParameters, true, path: varPath);
if (varDecl != null) {
varDeclarations.AddRange(varDecl);
} else {
@@ -831,7 +831,7 @@ public DMASTFile File() {
List varDeclarations = new();
TokenType type = isIndented ? TokenType.DM_Dedent : TokenType.DM_RightCurlyBracket;
while (!Check(type)) {
- DMASTProcStatementVarDeclaration[]? varDecl = ProcVarEnd(true, path: varPath);
+ DMASTProcStatementVarDeclaration[]? varDecl = ProcVarEnd(procParameters, true, path: varPath);
Delimiter();
Whitespace();
if (varDecl == null) {
@@ -857,12 +857,12 @@ public DMASTFile File() {
return null;
}
- private DMASTProcStatementVarDeclaration[]? ProcVarEnd(bool allowMultiple, DMASTPath? path = null) {
+ private DMASTProcStatementVarDeclaration[]? ProcVarEnd(List? procParameters, bool allowMultiple, DMASTPath? path = null) {
var loc = Current().Location;
DMASTPath? varPath = Path();
if (allowMultiple) {
- DMASTProcStatementVarDeclaration[]? block = ProcVarBlock(varPath);
+ DMASTProcStatementVarDeclaration[]? block = ProcVarBlock(procParameters, varPath);
if (block != null) return block;
}
@@ -884,7 +884,11 @@ public DMASTFile File() {
RequireExpression(ref value);
}
- var valType = AsComplexTypes() ?? DMValueType.Anything;
+ var valType = AsComplexTypes(procParameters);
+ // the != DMValueType.Null check is a hacky workaround for Anything|Null being equal to just Null
+ if (valType is null && value?.GetUnwrapped() is DMASTInput input && input.Types != DMValueType.Null) {
+ valType = input.Types;
+ }
varDeclarations.Add(new DMASTProcStatementVarDeclaration(loc, varPath, value, valType));
if (allowMultiple && Check(TokenType.DM_Comma)) {
@@ -1087,7 +1091,7 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
return null;
}
- private DMASTProcStatementSpawn? Spawn() {
+ private DMASTProcStatementSpawn? Spawn(List? procParameters = null) {
var loc = Current().Location;
if (Check(TokenType.DM_Spawn)) {
@@ -1110,9 +1114,9 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
Newline();
- DMASTProcBlockInner? body = ProcBlock();
+ DMASTProcBlockInner? body = ProcBlock(procParameters);
if (body == null) {
- DMASTProcStatement? statement = ProcStatement();
+ DMASTProcStatement? statement = ProcStatement(procParameters);
if (statement != null) {
body = new DMASTProcBlockInner(loc, statement);
@@ -1128,7 +1132,7 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
}
}
- private DMASTProcStatementIf? If() {
+ private DMASTProcStatementIf? If(List? procParameters = null) {
var loc = Current().Location;
if (Check(TokenType.DM_If)) {
@@ -1148,11 +1152,11 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
Check(TokenType.DM_Colon);
Whitespace();
- DMASTProcStatement? procStatement = ProcStatement();
+ DMASTProcStatement? procStatement = ProcStatement(procParameters);
DMASTProcBlockInner? elseBody = null;
DMASTProcBlockInner? body = (procStatement != null)
? new DMASTProcBlockInner(loc, procStatement)
- : ProcBlock();
+ : ProcBlock(procParameters);
body ??= new DMASTProcBlockInner(loc);
Token afterIfBody = Current();
@@ -1162,11 +1166,11 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
Whitespace();
Check(TokenType.DM_Colon);
Whitespace();
- procStatement = ProcStatement();
+ procStatement = ProcStatement(procParameters);
elseBody = (procStatement != null)
? new DMASTProcBlockInner(loc, procStatement)
- : ProcBlock();
+ : ProcBlock(procParameters);
elseBody ??= new DMASTProcBlockInner(loc);
} else if (newLineAfterIf) {
ReuseToken(afterIfBody);
@@ -1178,7 +1182,7 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
}
}
- private DMASTProcStatement? For() {
+ private DMASTProcStatement? For(List? procParameters = null) {
var loc = Current().Location;
if (Check(TokenType.DM_For)) {
@@ -1187,7 +1191,7 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
Whitespace();
if (Check(TokenType.DM_RightParenthesis)) {
- return new DMASTProcStatementInfLoop(loc, GetForBody(loc));
+ return new DMASTProcStatementInfLoop(loc, GetForBody(loc, procParameters));
}
_allowVarDeclExpression = true;
@@ -1207,7 +1211,7 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
if (expr1 is DMASTAssign assign) {
ExpressionTo(out var endRange, out var step);
Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after to expression");
- return new DMASTProcStatementFor(loc, new DMASTExpressionInRange(loc, assign.LHS, assign.RHS, endRange, step), null, null, dmTypes, GetForBody(loc));
+ return new DMASTProcStatementFor(loc, new DMASTExpressionInRange(loc, assign.LHS, assign.RHS, endRange, step), null, null, dmTypes, GetForBody(loc, procParameters));
} else {
Emit(WarningCode.BadExpression, "Expected = before to in for");
return new DMASTInvalidProcStatement(loc);
@@ -1219,16 +1223,16 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
DMASTExpression? listExpr = Expression();
Whitespace();
Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 2");
- return new DMASTProcStatementFor(loc, new DMASTExpressionIn(loc, expr1, listExpr), null, null, dmTypes, GetForBody(loc));
+ return new DMASTProcStatementFor(loc, new DMASTExpressionIn(loc, expr1, listExpr), null, null, dmTypes, GetForBody(loc, procParameters));
}
if (!Check(ForSeparatorTypes)) {
Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 1");
- return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody(loc));
+ return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody(loc, procParameters));
}
if (Check(TokenType.DM_RightParenthesis)) {
- return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody(loc));
+ return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody(loc, procParameters));
}
Whitespace();
@@ -1243,11 +1247,11 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
if (!Check(ForSeparatorTypes)) {
Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 2");
- return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody(loc));
+ return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody(loc, procParameters));
}
if (Check(TokenType.DM_RightParenthesis)) {
- return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody(loc));
+ return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody(loc, procParameters));
}
Whitespace();
@@ -1261,23 +1265,23 @@ private DMASTProcStatementSet[] ProcSetEnd(bool allowMultiple) {
}
Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 3");
- return new DMASTProcStatementFor(loc, expr1, expr2, expr3, dmTypes, GetForBody(loc));
+ return new DMASTProcStatementFor(loc, expr1, expr2, expr3, dmTypes, GetForBody(loc, procParameters));
}
return null;
- DMASTProcBlockInner GetForBody(Location forLocation) {
+ DMASTProcBlockInner GetForBody(Location forLocation, List? procParameters = null) {
Whitespace();
Newline();
- DMASTProcBlockInner? body = ProcBlock();
+ DMASTProcBlockInner? body = ProcBlock(procParameters);
if (body == null) {
var loc = Current().Location;
DMASTProcStatement? statement;
if (Check(TokenType.DM_Semicolon)) {
statement = new DMASTProcStatementExpression(loc, new DMASTConstantNull(loc));
} else {
- statement = ProcStatement();
+ statement = ProcStatement(procParameters);
if (statement == null) {
DMCompiler.Emit(WarningCode.MissingBody, forLocation, "Expected body or statement");
statement = new DMASTInvalidProcStatement(loc);
@@ -1291,7 +1295,7 @@ DMASTProcBlockInner GetForBody(Location forLocation) {
}
}
- private DMASTProcStatement? While() {
+ private DMASTProcStatement? While(List? procParameters = null) {
var loc = Current().Location;
if (Check(TokenType.DM_While)) {
@@ -1303,10 +1307,10 @@ DMASTProcBlockInner GetForBody(Location forLocation) {
ConsumeRightParenthesis();
Check(TokenType.DM_Semicolon);
Whitespace();
- DMASTProcBlockInner? body = ProcBlock();
+ DMASTProcBlockInner? body = ProcBlock(procParameters);
if (body == null) {
- DMASTProcStatement? statement = ProcStatement();
+ DMASTProcStatement? statement = ProcStatement(procParameters);
//Loops without a body are valid DM
statement ??= new DMASTProcStatementContinue(loc);
@@ -1324,7 +1328,7 @@ DMASTProcBlockInner GetForBody(Location forLocation) {
return null;
}
- private DMASTProcStatementDoWhile? DoWhile() {
+ private DMASTProcStatementDoWhile? DoWhile(List? procParameters = null) {
var loc = Current().Location;
if (Check(TokenType.DM_Do)) {
@@ -1359,7 +1363,7 @@ DMASTProcBlockInner GetForBody(Location forLocation) {
return null;
}
- private DMASTProcStatementSwitch? Switch() {
+ private DMASTProcStatementSwitch? Switch(List? procParameters = null) {
var loc = Current().Location;
if (Check(TokenType.DM_Switch)) {
@@ -1371,7 +1375,7 @@ DMASTProcBlockInner GetForBody(Location forLocation) {
ConsumeRightParenthesis();
Whitespace();
- DMASTProcStatementSwitch.SwitchCase[]? switchCases = SwitchCases();
+ DMASTProcStatementSwitch.SwitchCase[]? switchCases = SwitchCases(procParameters);
if (switchCases == null) {
switchCases = [];
Emit(WarningCode.MissingBody, "Expected switch cases");
@@ -1383,11 +1387,11 @@ DMASTProcBlockInner GetForBody(Location forLocation) {
return null;
}
- private DMASTProcStatementSwitch.SwitchCase[]? SwitchCases() {
+ private DMASTProcStatementSwitch.SwitchCase[]? SwitchCases(List? procParameters = null) {
Token beforeSwitchBlock = Current();
bool hasNewline = Newline();
- DMASTProcStatementSwitch.SwitchCase[]? switchCases = BracedSwitchInner() ?? IndentedSwitchInner();
+ DMASTProcStatementSwitch.SwitchCase[]? switchCases = BracedSwitchInner(procParameters) ?? IndentedSwitchInner(procParameters);
if (switchCases == null && hasNewline) {
ReuseToken(beforeSwitchBlock);
@@ -1396,12 +1400,12 @@ DMASTProcBlockInner GetForBody(Location forLocation) {
return switchCases;
}
- private DMASTProcStatementSwitch.SwitchCase[]? BracedSwitchInner() {
+ private DMASTProcStatementSwitch.SwitchCase[]? BracedSwitchInner(List? procParameters = null) {
if (Check(TokenType.DM_LeftCurlyBracket)) {
Whitespace();
Newline();
bool isIndented = Check(TokenType.DM_Indent);
- DMASTProcStatementSwitch.SwitchCase[] switchInner = SwitchInner();
+ DMASTProcStatementSwitch.SwitchCase[] switchInner = SwitchInner(procParameters);
if (isIndented) Check(TokenType.DM_Dedent);
Newline();
Consume(TokenType.DM_RightCurlyBracket, "Expected '}'");
@@ -1412,9 +1416,9 @@ DMASTProcBlockInner GetForBody(Location forLocation) {
return null;
}
- private DMASTProcStatementSwitch.SwitchCase[]? IndentedSwitchInner() {
+ private DMASTProcStatementSwitch.SwitchCase[]? IndentedSwitchInner(List? procParameters = null) {
if (Check(TokenType.DM_Indent)) {
- DMASTProcStatementSwitch.SwitchCase[] switchInner = SwitchInner();
+ DMASTProcStatementSwitch.SwitchCase[] switchInner = SwitchInner(procParameters);
Consume(TokenType.DM_Dedent, "Expected \"if\" or \"else\"");
return switchInner;
@@ -1423,21 +1427,21 @@ DMASTProcBlockInner GetForBody(Location forLocation) {
return null;
}
- private DMASTProcStatementSwitch.SwitchCase[] SwitchInner() {
+ private DMASTProcStatementSwitch.SwitchCase[] SwitchInner(List procParameters = null) {
List switchCases = new();
- DMASTProcStatementSwitch.SwitchCase? switchCase = SwitchCase();
+ DMASTProcStatementSwitch.SwitchCase? switchCase = SwitchCase(procParameters);
while (switchCase is not null) {
switchCases.Add(switchCase);
Newline();
Whitespace();
- switchCase = SwitchCase();
+ switchCase = SwitchCase(procParameters);
}
return switchCases.ToArray();
}
- private DMASTProcStatementSwitch.SwitchCase? SwitchCase() {
+ private DMASTProcStatementSwitch.SwitchCase? SwitchCase(List procParameters = null) {
if (Check(TokenType.DM_If)) {
List expressions = new();
@@ -1474,10 +1478,10 @@ private DMASTProcStatementSwitch.SwitchCase[] SwitchInner() {
Whitespace();
ConsumeRightParenthesis();
Whitespace();
- DMASTProcBlockInner? body = ProcBlock();
+ DMASTProcBlockInner? body = ProcBlock(procParameters);
if (body == null) {
- DMASTProcStatement? statement = ProcStatement();
+ DMASTProcStatement? statement = ProcStatement(procParameters);
body = (statement != null)
? new DMASTProcBlockInner(statement.Location, statement)
@@ -1495,10 +1499,10 @@ private DMASTProcStatementSwitch.SwitchCase[] SwitchInner() {
"Expected \"if\" or \"else\" - \"else if\" is ambiguous as a switch case and may cause unintended flow");
}
- DMASTProcBlockInner? body = ProcBlock();
+ DMASTProcBlockInner? body = ProcBlock(procParameters);
if (body == null) {
- DMASTProcStatement? statement = ProcStatement();
+ DMASTProcStatement? statement = ProcStatement(procParameters);
body = (statement != null)
? new DMASTProcBlockInner(loc, statement)
@@ -1511,15 +1515,15 @@ private DMASTProcStatementSwitch.SwitchCase[] SwitchInner() {
return null;
}
- private DMASTProcStatementTryCatch? TryCatch() {
+ private DMASTProcStatementTryCatch? TryCatch(List? procParameters = null) {
var loc = Current().Location;
if (Check(TokenType.DM_Try)) {
Whitespace();
- DMASTProcBlockInner? tryBody = ProcBlock();
+ DMASTProcBlockInner? tryBody = ProcBlock(procParameters);
if (tryBody == null) {
- DMASTProcStatement? statement = ProcStatement();
+ DMASTProcStatement? statement = ProcStatement(procParameters);
if (statement == null) {
statement = new DMASTInvalidProcStatement(loc);
Emit(WarningCode.MissingBody, "Expected body or statement");
@@ -1537,15 +1541,15 @@ private DMASTProcStatementSwitch.SwitchCase[] SwitchInner() {
DMASTProcStatement? parameter = null;
if (Check(TokenType.DM_LeftParenthesis)) {
BracketWhitespace();
- parameter = ProcVarDeclaration(allowMultiple: false);
+ parameter = ProcVarDeclaration(procParameters, allowMultiple: false);
BracketWhitespace();
ConsumeRightParenthesis();
Whitespace();
}
- DMASTProcBlockInner? catchBody = ProcBlock();
+ DMASTProcBlockInner? catchBody = ProcBlock(procParameters);
if (catchBody == null) {
- DMASTProcStatement? statement = ProcStatement();
+ DMASTProcStatement? statement = ProcStatement(procParameters);
if (statement != null) catchBody = new DMASTProcBlockInner(loc, statement);
}
@@ -2703,7 +2707,7 @@ private void BracketWhitespace() {
do {
Whitespace();
- type |= SingleAsType(out _);
+ type |= SingleAsType(out _, out _, out _);
Whitespace();
} while (Check(TokenType.DM_Bar));
@@ -2716,36 +2720,59 @@ private void BracketWhitespace() {
///
/// AsTypes(), but can handle more complex types such as type paths
+ /// If procParameters is non-null, allow as params[1] and as params[foo] syntax
///
- private DMComplexValueType? AsComplexTypes() {
+ private DMComplexValueType? AsComplexTypes(List? procParameters = null) {
if (!AsTypesStart(out var parenthetical))
return null;
if (parenthetical && Check(TokenType.DM_RightParenthesis)) // as ()
return DMValueType.Anything; // TODO: BYOND doesn't allow this for proc return types
+ var outType = UnionComplexTypes(procParameters);
+
+ if (parenthetical) {
+ ConsumeRightParenthesis();
+ }
+
+ return outType;
+ }
+
+ private DMComplexValueType? UnionComplexTypes(List? procParameters = null) {
DMValueType type = DMValueType.Anything;
DreamPath? path = null;
+ List<(int, bool)> paramIndices = new List<(int, bool)>();
+ DMListValueTypes? outListTypes = null;
do {
Whitespace();
- type |= SingleAsType(out var pathType, allowPath: true);
+ type |= SingleAsType(out var pathType, out var param, out var listTypes, allowPath: true, procParameters: procParameters);
Whitespace();
- if (pathType != null) {
- if (path == null)
+ if (param.paramIndex is not null) {
+ paramIndices.Add((param.paramIndex.Value, param.upcasted));
+ }
+ if (pathType is not null) {
+ if (path is null)
path = pathType;
- else
- DMCompiler.Emit(WarningCode.BadToken, CurrentLoc,
- $"Only one type path can be used, ignoring {pathType}");
+ else {
+ var newPath = path.Value.GetLastCommonAncestor(pathType.Value);
+ if (newPath != path) {
+ DMCompiler.Emit(WarningCode.LostTypeInfo, CurrentLoc,
+ $"Only one type path can be used, using last common ancestor {newPath}");
+ path = newPath;
+ }
+ }
+ }
+ if (listTypes is not null) {
+ if (outListTypes is null)
+ outListTypes = listTypes;
+ else {
+ outListTypes |= listTypes;
+ }
}
} while (Check(TokenType.DM_Bar));
-
- if (parenthetical) {
- ConsumeRightParenthesis();
- }
-
- return new(type, path);
+ return new(type, path, paramIndices.Count > 0 ? paramIndices.ToArray() : null, outListTypes);
}
private bool AsTypesStart(out bool parenthetical) {
@@ -2759,10 +2786,22 @@ private bool AsTypesStart(out bool parenthetical) {
return false;
}
- private DMValueType SingleAsType(out DreamPath? path, bool allowPath = false) {
+ private DMValueType SingleAsType(out DreamPath? path, out (int? paramIndex, bool upcasted) outParam, out DMListValueTypes? listTypes, bool allowPath = false, List? procParameters = null) {
Token typeToken = Current();
- if (!Check(new[] { TokenType.DM_Identifier, TokenType.DM_Null })) {
+ outParam = (null, false);
+ listTypes = null;
+
+ var inPath = false;
+ if (typeToken.Type is TokenType.DM_Identifier && typeToken.Text == "path") {
+ Advance();
+ if(Check(TokenType.DM_LeftParenthesis)) {
+ inPath = true;
+ Whitespace();
+ }
+ }
+
+ if (inPath || !Check(new[] { TokenType.DM_Identifier, TokenType.DM_Null })) {
// Proc return types
path = Path()?.Path;
if (allowPath) {
@@ -2770,11 +2809,30 @@ private DMValueType SingleAsType(out DreamPath? path, bool allowPath = false) {
DMCompiler.Emit(WarningCode.BadToken, typeToken.Location, "Expected value type or path");
}
- return DMValueType.Path;
+ if (inPath) {
+ Whitespace();
+ ConsumeRightParenthesis();
+ } else if (path == DreamPath.List) {
+ // check for list types
+ if (Check(TokenType.DM_LeftParenthesis)) {
+ DMComplexValueType? nestedKeyType = UnionComplexTypes(procParameters);
+ if (nestedKeyType is null)
+ DMCompiler.Emit(WarningCode.BadToken, CurrentLoc, "Expected value type or path");
+ DMComplexValueType? nestedValType = null;
+ if (Check(TokenType.DM_Comma)) { // Value
+ nestedValType = UnionComplexTypes(procParameters);
+ }
+ ConsumeRightParenthesis();
+ listTypes = new(nestedKeyType!.Value, nestedValType);
+ return DMValueType.Instance;
+ }
+ }
+
+ return inPath ? DMValueType.Path : DMValueType.Instance;
}
DMCompiler.Emit(WarningCode.BadToken, typeToken.Location, "Expected value type");
- return 0;
+ return DMValueType.Anything;
}
path = null;
@@ -2793,7 +2851,49 @@ private DMValueType SingleAsType(out DreamPath? path, bool allowPath = false) {
case "command_text": return DMValueType.CommandText;
case "sound": return DMValueType.Sound;
case "icon": return DMValueType.Icon;
- case "path": return DMValueType.Path;
+ case "params":
+ if (procParameters is null) {
+ DMCompiler.Emit(WarningCode.BadToken, typeToken.Location, "Cannot use 'as params' outside of a proc or its return type");
+ }
+ if (Check(TokenType.DM_LeftBracket)) {
+ Whitespace();
+ // Check for an identifier first
+ Token paramToken = Current();
+ switch (paramToken.Type) {
+ case TokenType.DM_Identifier:
+ outParam.paramIndex = procParameters?.FindIndex(x => x.Name == paramToken.Text);
+ if (outParam.paramIndex < 0) {
+ outParam.paramIndex = null;
+ }
+ break;
+ case TokenType.DM_Integer:
+ outParam.paramIndex = paramToken.ValueAsInt();
+ if (procParameters is null || (outParam.paramIndex is not null && outParam.paramIndex >= procParameters.Count)) {
+ DMCompiler.Emit(WarningCode.BadToken, paramToken.Location, $"Parameter index out of range ({outParam.paramIndex} >= {procParameters?.Count ?? 0})");
+ outParam.paramIndex = null;
+ }
+ break;
+ default:
+ DMCompiler.Emit(WarningCode.BadToken, paramToken.Location, $"Unrecognized parameter {paramToken.PrintableText}, expected parameter identifier or index");
+ break;
+ }
+ Advance();
+ Whitespace();
+ // Allow recasting a path to an instance with the instance keyword, here and only here
+ if(AsTypesStart(out var parenthetical)) {
+ Token nextToken = Current();
+ if (nextToken.Type is not TokenType.DM_Identifier || nextToken.Text != "instance")
+ DMCompiler.Emit(WarningCode.BadToken, nextToken.Location, $"Expected 'as instance', got 'as {nextToken.PrintableText}'");
+ else
+ outParam.upcasted = true;
+ Advance();
+ if (parenthetical)
+ ConsumeRightParenthesis();
+ }
+ ConsumeRightBracket();
+ }
+ path = null;
+ return DMValueType.Anything;
case "opendream_unimplemented": return DMValueType.Unimplemented;
case "opendream_compiletimereadonly": return DMValueType.CompiletimeReadonly;
default:
diff --git a/DMCompiler/DM/Builders/DMExpressionBuilder.cs b/DMCompiler/DM/Builders/DMExpressionBuilder.cs
index e51f0ff994..0fcf430bfa 100644
--- a/DMCompiler/DM/Builders/DMExpressionBuilder.cs
+++ b/DMCompiler/DM/Builders/DMExpressionBuilder.cs
@@ -385,7 +385,7 @@ private static DMExpression BuildIdentifier(DMASTIdentifier identifier, DMObject
if (CurrentScopeMode == ScopeMode.Normal) {
var localVar = proc?.GetLocalVariable(name);
if (localVar != null)
- return new Local(identifier.Location, localVar);
+ return new Local(identifier.Location, localVar, proc!, localVar.ExplicitValueType);
var field = dmObject?.GetVariable(name);
if (field != null) {
@@ -526,7 +526,7 @@ private static DMExpression BuildCallableProcIdentifier(DMASTCallableProcIdentif
if (CurrentScopeMode is ScopeMode.Static or ScopeMode.FirstPassStatic)
return new GlobalProc(procIdentifier.Location, procIdentifier.Identifier);
if (dmObject.HasProc(procIdentifier.Identifier))
- return new Proc(procIdentifier.Location, procIdentifier.Identifier);
+ return new Proc(procIdentifier.Location, procIdentifier.Identifier, dmObject);
if (DMObjectTree.TryGetGlobalProc(procIdentifier.Identifier, out _))
return new GlobalProc(procIdentifier.Location, procIdentifier.Identifier);
@@ -763,6 +763,7 @@ private static DMExpression BuildDereference(DMASTDereference deref, DMObject dm
case DMASTDereference.CallOperation callOperation: {
var field = callOperation.Identifier;
ArgumentList argumentList = new(deref.Expression.Location, dmObject, proc, callOperation.Parameters);
+ DreamPath? nextPath = null;
if (!callOperation.NoSearch && !pathIsFuzzy) {
if (prevPath == null) {
@@ -777,16 +778,25 @@ private static DMExpression BuildDereference(DMASTDereference deref, DMObject dm
if (!fromObject.HasProc(field))
return BadExpression(WarningCode.ItemDoesntExist, callOperation.Location,
$"Type {prevPath.Value} does not have a proc named \"{field}\"");
+
+ var returnTypes = fromObject.GetProcReturnTypes(field, argumentList) ?? DMValueType.Anything;
+ nextPath = returnTypes.HasPath ? returnTypes.TypePath : returnTypes.AsPath();
+ if (!returnTypes.HasPath & nextPath.HasValue) {
+ var thePath = nextPath!.Value;
+ thePath.Type = DreamPath.PathType.UpwardSearch;
+ nextPath = thePath;
+ }
}
operation = new Dereference.CallOperation {
Parameters = argumentList,
Safe = callOperation.Safe,
Identifier = field,
- Path = null
+ Path = prevPath
};
- prevPath = null;
- pathIsFuzzy = true;
+ prevPath = nextPath;
+ if(prevPath is null)
+ pathIsFuzzy = true;
break;
}
diff --git a/DMCompiler/DM/Builders/DMProcBuilder.cs b/DMCompiler/DM/Builders/DMProcBuilder.cs
index 01111ce837..e4c69b2482 100644
--- a/DMCompiler/DM/Builders/DMProcBuilder.cs
+++ b/DMCompiler/DM/Builders/DMProcBuilder.cs
@@ -1,4 +1,5 @@
using System.Diagnostics;
+using System.Linq;
using DMCompiler.Bytecode;
using DMCompiler.Compiler;
using DMCompiler.Compiler.DM;
@@ -16,6 +17,10 @@ internal sealed class DMProcBuilder(DMObject dmObject, DMProc proc) {
///
// Starts null; marks that we've never seen one before and should just error like normal people.
private Constant? _previousSetStatementValue;
+ ///
+ /// This tracks the current return type.
+ ///
+ private DMComplexValueType CurrentReturnType = DMValueType.Null;
public void ProcessProcDefinition(DMASTProcDefinition procDefinition) {
if (procDefinition.Body == null) return;
@@ -42,9 +47,43 @@ public void ProcessProcDefinition(DMASTProcDefinition procDefinition) {
}
ProcessBlockInner(procDefinition.Body, silenceEmptyBlockWarning : true);
+ // implicit return
+ CheckBlockReturnRecursive(procDefinition.Body);
proc.ResolveLabels();
}
+ private void CheckBlockReturnRecursive(DMASTProcBlockInner block) {
+ if (block.Statements.Length <= 0) {
+ proc.ValidateReturnType(CurrentReturnType, null, block.Location);
+ return;
+ }
+ var lastStatement = block.Statements[^1];
+ switch (lastStatement) {
+ case DMASTProcStatementIf ifStatement:
+ CheckBlockReturnRecursive(ifStatement.Body);
+ if (ifStatement.ElseBody is not null)
+ CheckBlockReturnRecursive(ifStatement.ElseBody);
+ else // if statement is non-exhaustive, so check for an implicit return after
+ proc.ValidateReturnType(CurrentReturnType, null, lastStatement.Location);
+ break;
+ case DMASTProcStatementSwitch switchStatement:
+ if (!switchStatement.Cases.Any()) // No cases to check?
+ break;
+ foreach (var switchCase in switchStatement.Cases) {
+ CheckBlockReturnRecursive(switchCase.Body);
+ }
+ if (!switchStatement.Cases.Any(x => x is DMASTProcStatementSwitch.SwitchCaseDefault)) // non-exhaustive, implicit return
+ proc.ValidateReturnType(CurrentReturnType, null, switchStatement.Cases.Last().Body.Location);
+ break;
+ case DMASTProcStatementReturn:
+ case DMASTProcStatementExpression expression when expression.Expression is DMASTAssign assign && assign.LHS is DMASTCallableSelf:
+ break; // already checked elsewhere
+ default:
+ proc.ValidateReturnType(CurrentReturnType, null, lastStatement.Location);
+ break;
+ }
+ }
+
/// Used to avoid emitting noisy warnings about procs with nothing in them.
/// FIXME: Eventually we should try to be smart enough to emit the error anyways for procs that
/// A.) are not marked opendream_unimplemented and
@@ -62,13 +101,13 @@ private void ProcessBlockInner(DMASTProcBlockInner block, bool silenceEmptyBlock
}
}
- if(!silenceEmptyBlockWarning && block.Statements.Length == 0) { // If this block has no real statements
+ if (!silenceEmptyBlockWarning && block.Statements.Length == 0) { // If this block has no real statements
// Not an error in BYOND, but we do have an emission for this!
if (block.SetStatements.Length != 0) {
// Give a more articulate message about this, since it's kinda weird
- DMCompiler.Emit(WarningCode.EmptyBlock,block.Location,"Empty block detected - set statements are executed outside of, before, and unconditional to, this block");
+ DMCompiler.Emit(WarningCode.EmptyBlock, block.Location, "Empty block detected - set statements are executed outside of, before, and unconditional to, this block");
} else {
- DMCompiler.Emit(WarningCode.EmptyBlock,block.Location,"Empty block detected");
+ DMCompiler.Emit(WarningCode.EmptyBlock, block.Location, "Empty block detected");
}
return;
@@ -130,7 +169,27 @@ public void ProcessStatement(DMASTProcStatement statement) {
}
public void ProcessStatementExpression(DMASTProcStatementExpression statement) {
- DMExpression.Emit(dmObject, proc, statement.Expression);
+ // this is terrible, can we please get an Emit override with an out parameter for the expression?
+ //DMExpression.Emit(dmObject, proc, statement.Expression);
+ var expr = DMExpression.Create(dmObject, proc, statement.Expression, null);
+ expr.EmitPushValue(dmObject, proc);
+ var checkedExpression = statement.Expression.GetUnwrapped();
+
+ switch (checkedExpression) {
+ case DMASTAssign astAssignment:
+ if (astAssignment.LHS is DMASTCallableSelf)
+ CurrentReturnType = expr.ValType;
+ break;
+ case DMASTAppend:
+ case DMASTLogicalOrAssign:
+ DMASTBinary astBinary = (DMASTBinary)checkedExpression;
+ if (astBinary.LHS is not DMASTCallableSelf)
+ break;
+ if (CurrentReturnType.Type != DMValueType.Null)
+ break;
+ CurrentReturnType |= expr.ValType;
+ break;
+ }
proc.Pop();
}
@@ -367,8 +426,9 @@ public void ProcessStatementVarDeclaration(DMASTProcStatementVarDeclaration varD
value = DMExpression.Create(dmObject, proc, varDeclaration.Value, varDeclaration.Type);
if (!varDeclaration.ValType.MatchesType(value.ValType)) {
- DMCompiler.Emit(WarningCode.InvalidVarType, varDeclaration.Location,
- $"{varDeclaration.Name}: Invalid var value {value.ValType}, expected {varDeclaration.ValType}");
+ if(!value.ValType.IsAnything || !DMCompiler.Settings.SkipAnythingTypecheck)
+ DMCompiler.Emit(WarningCode.InvalidVarType, varDeclaration.Location,
+ $"{varDeclaration.Name}: Invalid var value {value.ValType}, expected {varDeclaration.ValType}");
}
} else {
value = new Null(varDeclaration.Location);
@@ -383,7 +443,7 @@ public void ProcessStatementVarDeclaration(DMASTProcStatementVarDeclaration varD
successful = proc.TryAddLocalConstVariable(varDeclaration.Name, varDeclaration.Type, constValue);
} else {
- successful = proc.TryAddLocalVariable(varDeclaration.Name, varDeclaration.Type, varDeclaration.ValType);
+ successful = proc.TryAddLocalVariable(varDeclaration.Name, varDeclaration.Type, varDeclaration.ExplicitValType);
}
if (!successful) {
@@ -397,7 +457,7 @@ public void ProcessStatementVarDeclaration(DMASTProcStatementVarDeclaration varD
}
public void ProcessStatementReturn(DMASTProcStatementReturn statement) {
- if (statement.Value != null) {
+ if (statement.Value is not null and not DMASTCallableSelf) {
var expr = DMExpression.Create(dmObject, proc, statement.Value);
// Don't type-check unimplemented procs
@@ -411,6 +471,7 @@ public void ProcessStatementReturn(DMASTProcStatementReturn statement) {
expr.EmitPushValue(dmObject, proc);
} else {
+ proc.ValidateReturnType(CurrentReturnType, null, statement.Location);
proc.PushReferenceValue(DMReference.Self); //Default return value
}
@@ -418,7 +479,29 @@ public void ProcessStatementReturn(DMASTProcStatementReturn statement) {
}
public void ProcessStatementIf(DMASTProcStatementIf statement) {
- DMExpression.Emit(dmObject, proc, statement.Condition);
+ //DMExpression.Emit(dmObject, proc, statement.Condition);
+ // terrible code duplication from ProcessStatementExpression
+ // someone fix this in review please
+ var expr = DMExpression.Create(dmObject, proc, statement.Condition, null);
+ expr.EmitPushValue(dmObject, proc);
+ var checkedCondition = statement.Condition.GetUnwrapped();
+
+ switch (checkedCondition) {
+ case DMASTAssign astAssignment:
+ if (astAssignment.LHS is DMASTCallableSelf)
+ CurrentReturnType = expr.ValType;
+ break;
+ case DMASTAppend:
+ case DMASTLogicalOrAssign:
+ DMASTBinary astBinary = (DMASTBinary)checkedCondition;
+ if (astBinary.LHS is not DMASTCallableSelf)
+ break;
+ if (CurrentReturnType.Type != DMValueType.Null)
+ break;
+ CurrentReturnType |= expr.ValType;
+ break;
+ }
+ var oldReturnType = CurrentReturnType;
if (statement.ElseBody == null) {
string endLabel = proc.NewLabelName();
@@ -427,6 +510,7 @@ public void ProcessStatementIf(DMASTProcStatementIf statement) {
proc.StartScope();
ProcessBlockInner(statement.Body);
proc.EndScope();
+ CurrentReturnType = oldReturnType;
proc.AddLabel(endLabel);
} else {
string elseLabel = proc.NewLabelName();
@@ -437,9 +521,12 @@ public void ProcessStatementIf(DMASTProcStatementIf statement) {
proc.StartScope();
ProcessBlockInner(statement.Body);
proc.EndScope();
+ CurrentReturnType = oldReturnType;
proc.Jump(endLabel);
proc.AddLabel(elseLabel);
+ // else bodies are exhaustive, don't reset returntype
+ // if it's an elseif that's handled in ProcessBlockInner
proc.StartScope();
ProcessBlockInner(statement.ElseBody);
proc.EndScope();
@@ -451,7 +538,7 @@ public void ProcessStatementFor(DMASTProcStatementFor statementFor) {
proc.StartScope();
{
foreach (var decl in FindVarDecls(statementFor.Expression1)) {
- ProcessStatementVarDeclaration(new DMASTProcStatementVarDeclaration(statementFor.Location, decl.DeclPath, null, DMValueType.Anything));
+ ProcessStatementVarDeclaration(new DMASTProcStatementVarDeclaration(statementFor.Location, decl.DeclPath, null, null));
}
if (statementFor.Expression2 != null || statementFor.Expression3 != null) {
@@ -521,7 +608,10 @@ public void ProcessStatementFor(DMASTProcStatementFor statementFor) {
var list = DMExpression.Create(dmObject, proc, exprIn.RHS);
if (outputVar is Local outputLocal) {
- outputLocal.LocalVar.ExplicitValueType = statementFor.DMTypes;
+ if (statementFor.DMTypes is not null)
+ outputLocal.LocalVar.ExplicitValueType = statementFor.DMTypes;
+ else if (list.ValType.IsList && list.ValType.ListValueTypes is not null)
+ outputLocal.LocalVar.ExplicitValueType = list.ValType.ListValueTypes.NestedListKeyType;
}
ProcessStatementForList(list, outputVar, statementFor.DMTypes, statementFor.Body);
@@ -788,6 +878,28 @@ public void ProcessStatementSwitch(DMASTProcStatementSwitch statementSwitch) {
DMASTProcBlockInner? defaultCaseBody = null;
DMExpression.Emit(dmObject, proc, statementSwitch.Value);
+ //DMExpression.Emit(dmObject, proc, statement.Condition);
+ // terrible code duplication from ProcessStatementExpression
+ // someone fix this in review please
+ var expr = DMExpression.Create(dmObject, proc, statementSwitch.Value, null);
+ expr.EmitPushValue(dmObject, proc);
+ var checkedValue = statementSwitch.Value.GetUnwrapped();
+
+ switch (checkedValue) {
+ case DMASTAssign astAssignment:
+ if (astAssignment.LHS is DMASTCallableSelf)
+ CurrentReturnType = expr.ValType;
+ break;
+ case DMASTAppend:
+ case DMASTLogicalOrAssign:
+ DMASTBinary astBinary = (DMASTBinary)checkedValue;
+ if (astBinary.LHS is not DMASTCallableSelf)
+ break;
+ if (CurrentReturnType.Type != DMValueType.Null)
+ break;
+ CurrentReturnType |= expr.ValType;
+ break;
+ }
foreach (DMASTProcStatementSwitch.SwitchCase switchCase in statementSwitch.Cases) {
if (switchCase is DMASTProcStatementSwitch.SwitchCaseValues switchCaseValues) {
string caseLabel = proc.NewLabelName();
@@ -844,25 +956,33 @@ Constant CoerceBound(Constant bound) {
}
proc.Pop();
+ DMComplexValueType oldReturnType = CurrentReturnType;
+ DMComplexValueType? defaultCaseReturnType = null;
if (defaultCaseBody != null) {
+ // if a default case exists, the switch is exhaustive so we go with its returntype
proc.StartScope();
{
ProcessBlockInner(defaultCaseBody);
}
proc.EndScope();
+ defaultCaseReturnType = CurrentReturnType;
+ CurrentReturnType = oldReturnType; // the default case hasn't taken effect for the rest of the cases
}
proc.Jump(endLabel);
foreach ((string CaseLabel, DMASTProcBlockInner CaseBody) valueCase in valueCases) {
proc.AddLabel(valueCase.CaseLabel);
+ oldReturnType = CurrentReturnType;
proc.StartScope();
{
ProcessBlockInner(valueCase.CaseBody);
}
proc.EndScope();
+ CurrentReturnType = oldReturnType;
proc.Jump(endLabel);
}
+ CurrentReturnType = defaultCaseReturnType ?? oldReturnType;
proc.AddLabel(endLabel);
}
@@ -949,7 +1069,7 @@ public void ProcessStatementTryCatch(DMASTProcStatementTryCatch tryCatch) {
if (tryCatch.CatchParameter != null) {
var param = tryCatch.CatchParameter as DMASTProcStatementVarDeclaration;
- if (!proc.TryAddLocalVariable(param.Name, param.Type, param.ValType)) {
+ if (!proc.TryAddLocalVariable(param.Name, param.Type, param.ExplicitValType)) {
DMCompiler.Emit(WarningCode.DuplicateVariable, param.Location, $"Duplicate var {param.Name}");
}
diff --git a/DMCompiler/DM/DMExpression.cs b/DMCompiler/DM/DMExpression.cs
index 1c11e9d558..d5b97d6ab6 100644
--- a/DMCompiler/DM/DMExpression.cs
+++ b/DMCompiler/DM/DMExpression.cs
@@ -181,7 +181,6 @@ private static void VerifyArgType(DMProc targetProc, int index, string? name, DM
// TODO: Make a separate "UnsetStaticType" pragma for whether we should care if it's Anything
// TODO: We currently silently avoid typechecking "call()()" and "new" args (NewPath is handled)
// TODO: We currently don't handle variadic args (e.g. min())
- // TODO: Dereference.CallOperation does not pass targetProc
DMProc.LocalVariable? param;
if (name != null) {
@@ -202,7 +201,7 @@ private static void VerifyArgType(DMProc targetProc, int index, string? name, DM
DMComplexValueType paramType = param.ExplicitValueType ?? DMValueType.Anything;
- if (!expr.ValType.IsAnything && !paramType.MatchesType(expr.ValType)) {
+ if (!(DMCompiler.Settings.SkipAnythingTypecheck && expr.ValType.IsAnything) && !paramType.MatchesType(expr.ValType)) {
DMCompiler.Emit(WarningCode.InvalidVarType, expr.Location,
$"{targetProc.Name}(...) argument \"{param.Name}\": Invalid var value type {expr.ValType}, expected {paramType}");
}
diff --git a/DMCompiler/DM/DMObject.cs b/DMCompiler/DM/DMObject.cs
index f3841de6f5..dc93e26c15 100644
--- a/DMCompiler/DM/DMObject.cs
+++ b/DMCompiler/DM/DMObject.cs
@@ -95,16 +95,21 @@ public bool HasProcNoInheritance(string name) {
}
public DMComplexValueType? GetProcReturnTypes(string name) {
+
+ return GetProcReturnTypes(name, null);
+ }
+
+ public DMComplexValueType? GetProcReturnTypes(string name, ArgumentList? arguments) {
if (this == DMObjectTree.Root && DMObjectTree.TryGetGlobalProc(name, out var globalProc))
- return globalProc.RawReturnTypes;
+ return globalProc.GetParameterValueTypes(arguments);
if (GetProcs(name) is not { } procs)
- return Parent?.GetProcReturnTypes(name);
+ return Parent?.GetProcReturnTypes(name, arguments);
var proc = DMObjectTree.AllProcs[procs[0]];
if ((proc.Attributes & ProcAttributes.IsOverride) != 0)
- return Parent?.GetProcReturnTypes(name) ?? DMValueType.Anything;
+ return Parent?.GetProcReturnTypes(name, arguments) ?? DMValueType.Anything;
- return proc.RawReturnTypes;
+ return proc.GetParameterValueTypes(arguments);
}
public void AddVerb(DMProc verb) {
@@ -113,7 +118,7 @@ public void AddVerb(DMProc verb) {
}
public DMVariable CreateGlobalVariable(DreamPath? type, string name, bool isConst, DMComplexValueType? valType = null) {
- int id = DMObjectTree.CreateGlobal(out DMVariable global, type, name, isConst, valType ?? DMValueType.Anything);
+ int id = DMObjectTree.CreateGlobal(out DMVariable global, type, name, isConst, valType);
GlobalVariables[name] = id;
return global;
@@ -217,14 +222,12 @@ public bool IsSubtypeOf(DreamPath path) {
return Parent != null && Parent.IsSubtypeOf(path);
}
- public DMValueType GetDMValueType() {
- if (IsSubtypeOf(DreamPath.Mob))
- return DMValueType.Mob;
- if (IsSubtypeOf(DreamPath.Obj))
- return DMValueType.Obj;
- if (IsSubtypeOf(DreamPath.Area))
- return DMValueType.Area;
-
- return DMValueType.Anything;
+ public DreamPath GetLastCommonAncestor(DMObject other) {
+ if(other.IsSubtypeOf(Path)) {
+ return Path;
+ } else if(IsSubtypeOf(other.Path)) {
+ return other.Path;
+ }
+ return Parent?.GetLastCommonAncestor(other) ?? DreamPath.Root;
}
}
diff --git a/DMCompiler/DM/DMObjectTree.cs b/DMCompiler/DM/DMObjectTree.cs
index 9b08ac0ad1..a50f47299e 100644
--- a/DMCompiler/DM/DMObjectTree.cs
+++ b/DMCompiler/DM/DMObjectTree.cs
@@ -184,7 +184,7 @@ public static bool TryGetTypeId(DreamPath path, out int typeId) {
return null;
}
- public static int CreateGlobal(out DMVariable global, DreamPath? type, string name, bool isConst, DMComplexValueType valType) {
+ public static int CreateGlobal(out DMVariable global, DreamPath? type, string name, bool isConst, DMComplexValueType? valType) {
int id = Globals.Count;
global = new DMVariable(type, name, true, isConst, false, valType);
diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs
index a84715cfc1..f7895d3cd2 100644
--- a/DMCompiler/DM/DMProc.cs
+++ b/DMCompiler/DM/DMProc.cs
@@ -137,10 +137,13 @@ public void Compile() {
}
public void ValidateReturnType(DMExpression expr) {
- var type = expr.ValType;
+ ValidateReturnType(expr.ValType, expr, expr.Location);
+ }
+
+ public void ValidateReturnType(DMComplexValueType type, DMExpression? expr, Location location) {
var returnTypes = _dmObject.GetProcReturnTypes(Name)!.Value;
if ((returnTypes.Type & (DMValueType.Color | DMValueType.File | DMValueType.Message)) != 0) {
- DMCompiler.Emit(WarningCode.UnsupportedTypeCheck, expr.Location, "color, message, and file return types are currently unsupported.");
+ DMCompiler.Emit(WarningCode.UnsupportedTypeCheck, location, "color, message, and file return types are currently unsupported.");
return;
}
@@ -152,17 +155,19 @@ public void ValidateReturnType(DMExpression expr) {
switch (expr) {
case ProcCall:
- DMCompiler.Emit(WarningCode.InvalidReturnType, expr.Location, $"{_dmObject?.Path.ToString() ?? "Unknown"}.{Name}(): Called proc does not have a return type set, expected {ReturnTypes}.");
+ DMCompiler.Emit(WarningCode.InvalidReturnType, location, $"{_dmObject?.Path.ToString() ?? "Unknown"}.{Name}(): Called proc does not have a return type set, expected {ReturnTypes}.");
break;
case Local:
- DMCompiler.Emit(WarningCode.InvalidReturnType, expr.Location, $"{_dmObject?.Path.ToString() ?? "Unknown"}.{Name}(): Cannot determine return type of non-constant expression, expected {ReturnTypes}. Consider making this variable constant or adding an explicit \"as {ReturnTypes}\"");
+ DMCompiler.Emit(WarningCode.InvalidReturnType, location, $"{_dmObject?.Path.ToString() ?? "Unknown"}.{Name}(): Cannot determine return type of non-constant expression, expected {ReturnTypes}. Consider making this variable constant or adding an explicit \"as {ReturnTypes}\"");
+ break;
+ case null:
break;
default:
- DMCompiler.Emit(WarningCode.InvalidReturnType, expr.Location, $"{_dmObject?.Path.ToString() ?? "Unknown"}.{Name}(): Cannot determine return type of expression \"{expr}\", expected {ReturnTypes}. Consider reporting this as a bug on OpenDream's GitHub.");
+ DMCompiler.Emit(WarningCode.InvalidReturnType, location, $"{_dmObject?.Path.ToString() ?? "Unknown"}.{Name}(): Cannot determine return type of expression \"{expr}\", expected {ReturnTypes}. Consider reporting this as a bug on OpenDream's GitHub.");
break;
}
} else if (!ReturnTypes.MatchesType(type)) { // We could determine the return types but they don't match
- DMCompiler.Emit(WarningCode.InvalidReturnType, expr.Location, $"{_dmObject?.Path.ToString() ?? "Unknown"}{splitter}{Name}(): Invalid return type {type}, expected {ReturnTypes}");
+ DMCompiler.Emit(WarningCode.InvalidReturnType, location, $"{_dmObject?.Path.ToString() ?? "Unknown"}{splitter}{Name}(): Invalid return type {type}, expected {ReturnTypes}");
}
}
@@ -184,13 +189,13 @@ public ProcDefinitionJson GetJsonRepresentation() {
} else {
var type = DMObjectTree.GetDMObject(typePath, false);
- argumentType = type?.GetDMValueType() ?? DMValueType.Anything;
+ argumentType = type?.Path.GetAtomType() ?? DMValueType.Anything;
}
}
arguments.Add(new ProcArgumentJson {
Name = parameter.Name,
- Type = argumentType.Type
+ Type = argumentType.Type & ~(DMValueType.Instance|DMValueType.Path)
});
}
}
@@ -230,7 +235,7 @@ public void WaitFor(bool waitFor) {
}
public DMVariable CreateGlobalVariable(DreamPath? type, string name, bool isConst, out int id) {
- id = DMObjectTree.CreateGlobal(out DMVariable global, type, name, isConst, DMValueType.Anything);
+ id = DMObjectTree.CreateGlobal(out DMVariable global, type, name, isConst, null);
GlobalVariables[name] = id;
return global;
@@ -280,7 +285,7 @@ public bool TryGetParameterAtIndex(int index, [NotNullWhen(true)] out LocalVaria
return label;
}
- public bool TryAddLocalVariable(string name, DreamPath? type, DMComplexValueType valType) {
+ public bool TryAddLocalVariable(string name, DreamPath? type, DMComplexValueType? valType) {
if (_parameters.ContainsKey(name)) //Parameters and local vars cannot share a name
return false;
@@ -318,6 +323,57 @@ public DMReference GetLocalVariableReference(string name) {
return local.IsParameter ? DMReference.CreateArgument(local.Id) : DMReference.CreateLocal(local.Id);
}
+ public DMProc GetBaseProc(DMObject? dmObject = null) {
+ if (dmObject == null) dmObject = _dmObject;
+ if (dmObject == DMObjectTree.Root && DMObjectTree.TryGetGlobalProc(Name, out var globalProc))
+ return globalProc;
+ if (dmObject.GetProcs(Name) is not { } procs)
+ return dmObject.Parent is not null ? GetBaseProc(dmObject.Parent) : this;
+
+ var proc = DMObjectTree.AllProcs[procs[0]];
+ if ((proc.Attributes & ProcAttributes.IsOverride) != 0)
+ return dmObject.Parent is not null ? GetBaseProc(dmObject.Parent) : this;
+
+ return proc;
+ }
+
+ public DMComplexValueType GetParameterValueTypes(ArgumentList? arguments) {
+ return GetParameterValueTypes(RawReturnTypes, arguments);
+ }
+
+ public DMComplexValueType GetParameterValueTypes(DMComplexValueType? baseType, ArgumentList? arguments) {
+ if (baseType?.ParameterIndices is null) {
+ return baseType ?? DMValueType.Anything;
+ }
+ DMComplexValueType returnType = baseType ?? DMValueType.Anything;
+ foreach ((int parameterIndex, bool upcasted) in baseType!.Value.ParameterIndices) {
+ DMComplexValueType intermediateType = DMValueType.Anything;
+ if (arguments is null || parameterIndex >= arguments.Expressions.Length) {
+ if (!TryGetParameterAtIndex(parameterIndex, out var parameter)) {
+ DMCompiler.Emit(WarningCode.BadArgument, Location, $"Unable to find argument with index {parameterIndex}");
+ continue;
+ }
+ intermediateType = parameter.ExplicitValueType ?? DMValueType.Anything;
+ } else if (arguments is not null) {
+ intermediateType = arguments.Expressions[parameterIndex].Expr.ValType;
+ }
+ if (upcasted) {
+ if (intermediateType.HasPath) {
+ if (intermediateType.Type.HasFlag(~(DMValueType.Path | DMValueType.Null)))
+ DMCompiler.Emit(WarningCode.InvalidVarType, arguments?.Location ?? Location, "Expected an exclusively path (or null) typed parameter");
+ else
+ intermediateType = new DMComplexValueType((intermediateType.Type & DMValueType.Null) | DMValueType.Instance, intermediateType.TypePath);
+ } else if (DMCompiler.Settings.SkipAnythingTypecheck && intermediateType.IsAnything) {
+ //pass
+ } else {
+ DMCompiler.Emit(WarningCode.InvalidVarType, arguments?.Location ?? Location, "Expected a path (or path|null) typed parameter");
+ }
+ }
+ returnType |= intermediateType;
+ }
+ return returnType;
+ }
+
public void Error() {
WriteOpcode(DreamProcOpcode.Error);
}
diff --git a/DMCompiler/DM/DMValueType.cs b/DMCompiler/DM/DMValueType.cs
index e521524bb7..d06497b57d 100644
--- a/DMCompiler/DM/DMValueType.cs
+++ b/DMCompiler/DM/DMValueType.cs
@@ -23,11 +23,12 @@ public enum DMValueType {
CommandText = 0x400,
Sound = 0x800,
Icon = 0x1000,
- Path = 0x2000, // For proc return types
+ Instance = 0x2000, // For proc return types
+ Path = 0x4000,
//Byond here be dragons
- Unimplemented = 0x4000, // Marks that a method or property is not implemented. Throws a compiler warning if accessed.
- CompiletimeReadonly = 0x8000, // Marks that a property can only ever be read from, never written to. This is a const-ier version of const, for certain standard values like list.type
+ Unimplemented = 0x8000, // Marks that a method or property is not implemented. Throws a compiler warning if accessed.
+ CompiletimeReadonly = 0x10000, // Marks that a property can only ever be read from, never written to. This is a const-ier version of const, for certain standard values like list.type
}
///
@@ -36,11 +37,25 @@ public enum DMValueType {
public readonly struct DMComplexValueType {
public readonly DMValueType Type;
public readonly DreamPath? TypePath;
+ ///
+ /// The indices of the proc parameters used to infer proc return types.
+ /// The resulting type is the union of all the parameter types with `this.Type`.
+ /// If two or more of those types have paths, their common ancestor is used.
+ ///
+ public readonly (int, bool)[]? ParameterIndices;
public bool IsAnything => Type == DMValueType.Anything;
- public bool IsPath => Type.HasFlag(DMValueType.Path);
+ public bool IsInstance => Type.HasFlag(DMValueType.Instance);
+ public bool HasPath => Type.HasFlag(DMValueType.Path) | Type.HasFlag(DMValueType.Instance);
public bool IsUnimplemented { get; }
public bool IsCompileTimeReadOnly { get; }
+ public bool IsList => IsInstance && TypePath == DreamPath.List;
+ ///
+ /// A pointer to a class wrapping the key and value DMComplexValueTypes for a list.
+ /// This cannot be a struct because that would create a cycle in the struct representation.
+ /// Sorry about the heap allocation.
+ ///
+ public readonly DMListValueTypes? ListValueTypes;
public DMComplexValueType(DMValueType type, DreamPath? typePath) {
Type = type & ~(DMValueType.Unimplemented | DMValueType.CompiletimeReadonly); // Ignore these 2 types
@@ -48,35 +63,125 @@ public DMComplexValueType(DMValueType type, DreamPath? typePath) {
IsUnimplemented = type.HasFlag(DMValueType.Unimplemented);
IsCompileTimeReadOnly = type.HasFlag(DMValueType.CompiletimeReadonly);
- if (IsPath && TypePath == null)
- throw new Exception("A Path value type must have a type-path");
+ if (HasPath && TypePath == null)
+ throw new Exception("A Path or Instance value type must have a type-path");
}
+ public DMComplexValueType(DMValueType type, DreamPath? typePath, (int, bool)[]? parameterIndices) : this(type, typePath) {
+ ParameterIndices = parameterIndices;
+ }
+
+ public DMComplexValueType(DMValueType type, DreamPath? typePath, (int, bool)[]? parameterIndices, DMListValueTypes? listValueTypes) : this(type, typePath, parameterIndices) {
+ ListValueTypes = listValueTypes;
+ }
+
+ public DMComplexValueType(DMValueType type, DreamPath? typePath, DMListValueTypes? listValueTypes) : this(type, typePath, null, listValueTypes) { }
+
public bool MatchesType(DMValueType type) {
- return IsAnything || (Type & type) != 0;
+ if (IsAnything || (Type & type) != 0) return true;
+ if ((type & (DMValueType.Text | DMValueType.Message)) != 0 && (Type & (DMValueType.Text | DMValueType.Message)) != 0) return true;
+ if ((type & (DMValueType.Text | DMValueType.Color)) != 0 && (Type & (DMValueType.Text | DMValueType.Color)) != 0) return true;
+ return false;
}
public bool MatchesType(DMComplexValueType type) {
- if (IsPath && type.IsPath) {
+ // Exclude checking path and null here; primitives only.
+ if (MatchesType(type.Type & ~(DMValueType.Path|DMValueType.Instance|DMValueType.Null)))
+ return true;
+ // If we have a /icon, we have an icon; if we have a /obj, we have an obj; etc.
+ if (IsInstance) {
+ if (type.MatchesType(TypePath!.Value.GetAtomType())) {
+ return true;
+ }
+ var theirPath = type.AsPath();
+ if (type.ListValueTypes is null && theirPath is not null) {
+ var theirObject = DMObjectTree.GetDMObject(theirPath!.Value, false);
+ if (theirObject?.IsSubtypeOf(TypePath!.Value) is true) {
+ return true;
+ }
+ }
+ }
+ // special case for color and lists:
+ if (type.Type.HasFlag(DMValueType.Color) && IsList && ListValueTypes?.NestedListKeyType.Type == DMValueType.Num && ListValueTypes.NestedListValType is null)
+ return true;
+ // probably only one of these is correct but i can't be assed to figure out which
+ if (Type.HasFlag(DMValueType.Color) && type.IsList && type.ListValueTypes?.NestedListKeyType.Type == DMValueType.Num && type.ListValueTypes.NestedListValType is null)
+ return true;
+ if (type.IsInstance && MatchesType(type.TypePath!.Value.GetAtomType())) {
+ return true;
+ }
+ if (HasPath && type.HasPath) {
var dmObject = DMObjectTree.GetDMObject(type.TypePath!.Value, false);
// Allow subtypes
- if (dmObject?.IsSubtypeOf(TypePath!.Value) is true)
- return true;
+ if (dmObject?.IsSubtypeOf(TypePath!.Value) is false) {
+ var ourObject = DMObjectTree.GetDMObject(TypePath!.Value, false);
+ return ourObject?.IsSubtypeOf(type.TypePath!.Value) ?? false;
+ }
+ // If ListValueTypes is non-null, we do more advanced checks.
+ // The other type's must also be non-null, because we consider empty lists as matching.
+ if (ListValueTypes is not null && type.ListValueTypes is not null) {
+ // Have to do an actual match check here. This can get expensive, but thankfully it's pretty rare.
+ if (!ListValueTypes.NestedListKeyType.MatchesType(type.ListValueTypes.NestedListKeyType))
+ return false;
+ // If we're assoc (have value types rather than just keys), then the other list must match as well.
+ if (ListValueTypes?.NestedListValType is not null) {
+ if (type.ListValueTypes!.NestedListValType is not null && !ListValueTypes.NestedListValType!.Value.MatchesType(type.ListValueTypes!.NestedListValType.Value))
+ return false;
+ if (type.ListValueTypes!.NestedListValType is null && !ListValueTypes.NestedListValType!.Value.MatchesType(DMValueType.Null))
+ return false;
+ }
+ }
}
-
return MatchesType(type.Type);
}
public override string ToString() {
var types = Type.ToString().ToLowerInvariant();
- return $"\"{(IsPath ? types + ", " + TypePath!.Value : types)}\"";
+ return $"\"{(HasPath ? types + $", {TypePath!.Value}{((IsList && ListValueTypes is not null) ? $"({ListValueTypes})" : "")}" : types)}\"";
}
public static implicit operator DMComplexValueType(DMValueType type) => new(type, null);
- public static implicit operator DMComplexValueType(DreamPath path) => new(DMValueType.Path, path);
+ public static implicit operator DMComplexValueType(DreamPath path) => new(DMValueType.Instance, path);
public static DMComplexValueType operator |(DMComplexValueType type1, DMValueType type2) =>
new(type1.Type | type2, type1.TypePath);
+
+ public static DMComplexValueType operator |(DMComplexValueType type1, DMComplexValueType type2) {
+ if (type2.TypePath is null) {
+ return type1 | type2.Type;
+ } else if (type1.TypePath is null) {
+ return type2 | type1.Type;
+ }
+ // Take the common ancestor of both types
+ return new(type1.Type | type2.Type, type1.TypePath.Value.GetLastCommonAncestor(type2.TypePath.Value));
+ }
+
+ public DreamPath? AsPath() {
+ return (HasPath ? TypePath : null) ?? (Type & ~DMValueType.Null) switch {
+ DMValueType.Mob => DreamPath.Mob,
+ DMValueType.Icon => DreamPath.Icon,
+ DMValueType.Obj => DreamPath.Obj,
+ DMValueType.Turf => DreamPath.Turf,
+ DMValueType.Area => DreamPath.Area,
+ DMValueType.Obj | DMValueType.Mob => DreamPath.Movable,
+ DMValueType.Area | DMValueType.Turf | DMValueType.Obj | DMValueType.Mob => DreamPath.Atom,
+ DMValueType.Sound => DreamPath.Sound,
+ _ => null
+ };
+ }
+}
+
+public class DMListValueTypes(DMComplexValueType nestedListKeyType, DMComplexValueType? nestedListValType) {
+ public DMComplexValueType NestedListKeyType => nestedListKeyType;
+ public DMComplexValueType? NestedListValType => nestedListValType;
+ public static DMListValueTypes operator |(DMListValueTypes type1, DMListValueTypes type2) {
+ return new(type1.NestedListKeyType | type2.NestedListKeyType, type1.NestedListValType | type2.NestedListValType);
+ }
+ public override string ToString() {
+ if (NestedListValType is not null)
+ return $"{NestedListKeyType} = {NestedListValType}";
+ return NestedListKeyType.ToString();
+ }
}
diff --git a/DMCompiler/DM/DMVariable.cs b/DMCompiler/DM/DMVariable.cs
index 86510b08d1..8f30a488df 100644
--- a/DMCompiler/DM/DMVariable.cs
+++ b/DMCompiler/DM/DMVariable.cs
@@ -21,7 +21,8 @@ public DMVariable(DreamPath? type, string name, bool isGlobal, bool isConst, boo
IsConst = isConst;
IsTmp = isTmp;
Value = null;
- ValType = valType ?? DMValueType.Anything;
+ DMComplexValueType atomType = Type is not null ? new DMComplexValueType(DMValueType.Instance | DMValueType.Path, Type) : DMValueType.Anything;
+ ValType = valType ?? (!atomType.IsAnything ? atomType | DMValueType.Null : (Type is null ? DMValueType.Anything : new DMComplexValueType(DMValueType.Instance | DMValueType.Path | DMValueType.Null, Type)));
}
///
diff --git a/DMCompiler/DM/Expressions/Binary.cs b/DMCompiler/DM/Expressions/Binary.cs
index 1b05d20317..a21c3d4215 100644
--- a/DMCompiler/DM/Expressions/Binary.cs
+++ b/DMCompiler/DM/Expressions/Binary.cs
@@ -322,6 +322,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// x == y
internal sealed class Equal(Location location, DMExpression lhs, DMExpression rhs) : BinaryOp(location, lhs, rhs) {
+ public override DMComplexValueType ValType => DMValueType.Num; // todo: DMValueType.Bool
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
LHS.EmitPushValue(dmObject, proc);
RHS.EmitPushValue(dmObject, proc);
@@ -331,6 +332,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// x != y
internal sealed class NotEqual(Location location, DMExpression lhs, DMExpression rhs) : BinaryOp(location, lhs, rhs) {
+ public override DMComplexValueType ValType => DMValueType.Num; // todo: DMValueType.Bool
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
LHS.EmitPushValue(dmObject, proc);
RHS.EmitPushValue(dmObject, proc);
@@ -340,6 +342,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// x ~= y
internal sealed class Equivalent(Location location, DMExpression lhs, DMExpression rhs) : BinaryOp(location, lhs, rhs) {
+ public override DMComplexValueType ValType => DMValueType.Num; // todo: DMValueType.Bool
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
LHS.EmitPushValue(dmObject, proc);
RHS.EmitPushValue(dmObject, proc);
@@ -349,6 +352,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// x ~! y
internal sealed class NotEquivalent(Location location, DMExpression lhs, DMExpression rhs) : BinaryOp(location, lhs, rhs) {
+ public override DMComplexValueType ValType => DMValueType.Num; // todo: DMValueType.Bool
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
LHS.EmitPushValue(dmObject, proc);
RHS.EmitPushValue(dmObject, proc);
@@ -358,6 +362,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// x > y
internal sealed class GreaterThan(Location location, DMExpression lhs, DMExpression rhs) : BinaryOp(location, lhs, rhs) {
+ public override DMComplexValueType ValType => DMValueType.Num; // todo: DMValueType.Bool
public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) {
if (!LHS.TryAsConstant(out var lhs) || !RHS.TryAsConstant(out var rhs)) {
constant = null;
@@ -385,6 +390,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// x >= y
internal sealed class GreaterThanOrEqual(Location location, DMExpression lhs, DMExpression rhs) : BinaryOp(location, lhs, rhs) {
+ public override DMComplexValueType ValType => DMValueType.Num; // todo: DMValueType.Bool
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
LHS.EmitPushValue(dmObject, proc);
RHS.EmitPushValue(dmObject, proc);
@@ -413,6 +419,7 @@ public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) {
// x < y
internal sealed class LessThan(Location location, DMExpression lhs, DMExpression rhs) : BinaryOp(location, lhs, rhs) {
+ public override DMComplexValueType ValType => DMValueType.Num; // todo: DMValueType.Bool
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
LHS.EmitPushValue(dmObject, proc);
RHS.EmitPushValue(dmObject, proc);
@@ -440,6 +447,7 @@ public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) {
// x <= y
internal sealed class LessThanOrEqual(Location location, DMExpression lhs, DMExpression rhs) : BinaryOp(location, lhs, rhs) {
+ public override DMComplexValueType ValType => DMValueType.Num; // todo: DMValueType.Bool
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
LHS.EmitPushValue(dmObject, proc);
RHS.EmitPushValue(dmObject, proc);
@@ -496,6 +504,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// x && y
internal sealed class And(Location location, DMExpression lhs, DMExpression rhs) : BinaryOp(location, lhs, rhs) {
+ public override DMComplexValueType ValType => RHS.ValType;
public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) {
if (LHS.TryAsConstant(out var lhs) && !lhs.IsTruthy()) {
constant = lhs;
@@ -523,6 +532,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// x in y
internal sealed class In(Location location, DMExpression expr, DMExpression container) : BinaryOp(location, expr, container) {
+ public override DMComplexValueType ValType => DMValueType.Num; // todo: DMValueType.Bool
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
LHS.EmitPushValue(dmObject, proc);
RHS.EmitPushValue(dmObject, proc);
@@ -554,6 +564,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// x = y
internal sealed class Assignment(Location location, DMExpression lhs, DMExpression rhs) : AssignmentBinaryOp(location, lhs, rhs) {
public override DreamPath? Path => LHS.Path;
+ public override DMComplexValueType ValType => RHS.ValType;
protected override void EmitOp(DMObject dmObject, DMProc proc, DMReference reference, string endLabel) {
RHS.EmitPushValue(dmObject, proc);
@@ -581,6 +592,7 @@ protected override void EmitOp(DMObject dmObject, DMProc proc, DMReference refer
// x += y
internal sealed class Append(Location location, DMExpression lhs, DMExpression rhs) : AssignmentBinaryOp(location, lhs, rhs) {
+ public override DMComplexValueType ValType => LHS.ValType.Type == DMValueType.Null ? RHS.ValType : RHS.ValType;
protected override void EmitOp(DMObject dmObject, DMProc proc, DMReference reference, string endLabel) {
RHS.EmitPushValue(dmObject, proc);
proc.Append(reference);
diff --git a/DMCompiler/DM/Expressions/Builtins.cs b/DMCompiler/DM/Expressions/Builtins.cs
index f09ab45566..6317fed4fa 100644
--- a/DMCompiler/DM/Expressions/Builtins.cs
+++ b/DMCompiler/DM/Expressions/Builtins.cs
@@ -46,7 +46,16 @@ public void EmitPushArglist(DMObject dmObject, DMProc proc) {
// new x (...)
internal sealed class New(Location location, DMExpression expr, ArgumentList arguments) : DMExpression(location) {
public override bool PathIsFuzzy => Path == null;
- public override DMComplexValueType ValType => !expr.ValType.IsAnything ? expr.ValType : (Path?.GetAtomType() ?? DMValueType.Anything);
+ public override DMComplexValueType ValType {
+ get {
+ var exprType = expr.ValType;
+ if (exprType.IsAnything)
+ return Path is not null ? new DMComplexValueType(DMValueType.Instance, Path) : DMValueType.Anything;
+ if (exprType.HasPath && exprType.Type.HasFlag(DMValueType.Path))
+ return exprType.TypePath is not null ? new DMComplexValueType(DMValueType.Instance, exprType.TypePath) : DMValueType.Anything;
+ return exprType;
+ }
+ }
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
var argumentInfo = arguments.EmitArguments(dmObject, proc, null);
@@ -59,10 +68,11 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// new /x/y/z (...)
internal sealed class NewPath(Location location, ConstantPath targetPath, ArgumentList arguments) : DMExpression(location) {
public override DreamPath? Path => targetPath.Value;
- public override DMComplexValueType ValType => targetPath.Value.GetAtomType();
+ public override DMComplexValueType ValType => Path is not null ? new DMComplexValueType(DMValueType.Instance | DMValueType.Null, Path) : DMValueType.Anything;
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
if (!targetPath.TryResolvePath(out var pathInfo)) {
+ DMCompiler.Emit(WarningCode.BadExpression, Location, "Invalid path to new /datum()");
proc.PushNull();
return;
}
@@ -98,7 +108,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// locate()
internal sealed class LocateInferred(Location location, DreamPath path, DMExpression? container) : DMExpression(location) {
- public override DMComplexValueType ValType => path;
+ public override DMComplexValueType ValType => path.GetAtomType();
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
if (!DMObjectTree.TryGetTypeId(path, out var typeId)) {
@@ -129,6 +139,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// locate(x)
internal sealed class Locate(Location location, DMExpression path, DMExpression? container) : DMExpression(location) {
public override bool PathIsFuzzy => true;
+ public override DMComplexValueType ValType => path.Path is not null ? new DMComplexValueType(DMValueType.Instance | DMValueType.Null, path.Path) : DMValueType.Anything;
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
path.EmitPushValue(dmObject, proc);
@@ -178,6 +189,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
/// rgb(x, y, z, space)
/// rgb(x, y, z, a, space)
internal sealed class Rgb(Location location, ArgumentList arguments) : DMExpression(location) {
+ public override DMComplexValueType ValType => DMValueType.Color;
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
DMObjectTree.TryGetGlobalProc("rgb", out var dmProc);
var argInfo = arguments.EmitArguments(dmObject, proc, dmProc);
@@ -195,6 +207,22 @@ public struct PickValue(DMExpression? weight, DMExpression value) {
public readonly DMExpression Value = value;
}
+ public override DMComplexValueType ValType {
+ get {
+ if (values.Length == 1) {
+ var firstValType = values[0].Value.ValType;
+ if (firstValType.IsList) {
+ return firstValType.ListValueTypes?.NestedListKeyType ?? DMValueType.Anything;
+ }
+ }
+ DMComplexValueType accumValues = DMValueType.Anything;
+ foreach(PickValue pickVal in values) {
+ accumValues |= pickVal.Value.ValType;
+ }
+ return accumValues;
+ }
+ }
+
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
bool weighted = false;
foreach (PickValue pickValue in values) {
@@ -340,6 +368,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// get_step(ref, dir)
internal sealed class GetStep(Location location, DMExpression refValue, DMExpression dir) : DMExpression(location) {
public override bool PathIsFuzzy => true;
+ public override DMComplexValueType ValType => DMValueType.Turf | DMValueType.Null;
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
refValue.EmitPushValue(dmObject, proc);
@@ -351,6 +380,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// get_dir(loc1, loc2)
internal sealed class GetDir(Location location, DMExpression loc1, DMExpression loc2) : DMExpression(location) {
public override bool PathIsFuzzy => true;
+ public override DMComplexValueType ValType => DMValueType.Num; // invalid/no dir is 0, not null
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
loc1.EmitPushValue(dmObject, proc);
@@ -365,7 +395,19 @@ internal sealed class List : DMExpression {
private readonly bool _isAssociative;
public override bool PathIsFuzzy => true;
- public override DMComplexValueType ValType => DreamPath.List;
+ public override DMComplexValueType ValType {
+ get {
+ DMComplexValueType keyTypes = DMValueType.Anything;
+ DMComplexValueType valTypes = DMValueType.Anything;
+ foreach((DMExpression? key, DMExpression val) in _values) {
+ // why did I do it like this
+ valTypes |= val.ValType;
+ if(key is not null)
+ keyTypes |= key.ValType;
+ }
+ return new DMComplexValueType(DMValueType.Instance, DreamPath.List, !(keyTypes.IsAnything && valTypes.IsAnything) ? (_isAssociative ? new DMListValueTypes(keyTypes, valTypes) : new DMListValueTypes(valTypes, null)) : null);
+ }
+ }
public List(Location location, (DMExpression? Key, DMExpression Value)[] values) : base(location) {
_values = values;
@@ -435,6 +477,7 @@ public override bool TryAsJsonRepresentation(out object? json) {
// Value of var/list/L[1][2][3]
internal sealed class DimensionalList(Location location, DMExpression[] sizes) : DMExpression(location) {
+ public override DMComplexValueType ValType => DreamPath.List;
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// This basically emits new /list(1, 2, 3)
diff --git a/DMCompiler/DM/Expressions/Constant.cs b/DMCompiler/DM/Expressions/Constant.cs
index 6b4cb135d9..14a9a888dc 100644
--- a/DMCompiler/DM/Expressions/Constant.cs
+++ b/DMCompiler/DM/Expressions/Constant.cs
@@ -101,6 +101,7 @@ internal sealed class Resource : Constant {
private readonly string _filePath;
private bool _isAmbiguous;
+ public override DMComplexValueType ValType { get; }
public Resource(Location location, string filePath) : base(location) {
// Treat backslashes as forward slashes on Linux
@@ -155,6 +156,17 @@ public Resource(Location location, string filePath) : base(location) {
_filePath = _filePath.Replace('\\', '/');
DMObjectTree.Resources.Add(_filePath);
+
+ ValType = System.IO.Path.GetExtension(fileName) switch {
+ ".dmi" => DMValueType.Icon,
+ ".png" => DMValueType.Icon,
+ ".bmp" => DMValueType.Icon,
+ ".gif" => DMValueType.Icon,
+ ".ogg" => DMValueType.Sound,
+ ".wav" => DMValueType.Sound,
+ ".mid" => DMValueType.Sound,
+ _ => DMValueType.File
+ };
}
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
@@ -230,7 +242,7 @@ internal sealed class ConstantPath(Location location, DMObject dmObject, DreamPa
private readonly DMObject _dmObject = dmObject;
public override DreamPath? Path => Value;
- public override DMComplexValueType ValType => Value;
+ public override DMComplexValueType ValType => new DMComplexValueType(DMValueType.Path, Value);
public enum PathType {
TypeReference,
diff --git a/DMCompiler/DM/Expressions/Dereference.cs b/DMCompiler/DM/Expressions/Dereference.cs
index 33553f9102..298f8eb458 100644
--- a/DMCompiler/DM/Expressions/Dereference.cs
+++ b/DMCompiler/DM/Expressions/Dereference.cs
@@ -36,6 +36,18 @@ public sealed class IndexOperation : Operation {
/// The index expression. (eg. x[expr])
///
public required DMExpression Index { get; init; }
+ public DMComplexValueType UnnestValType(DMListValueTypes? listValueTypes) {
+ if (listValueTypes is null) return DMValueType.Anything;
+ if (listValueTypes.NestedListValType is null) return listValueTypes.NestedListKeyType | DMValueType.Null;
+ return Index.ValType.Type switch {
+ // if Index.ValType is only null, we return null
+ DMValueType.Null => DMValueType.Null,
+ // if it's only num, return key
+ DMValueType.Num => listValueTypes.NestedListKeyType,
+ // else, return valtype|null
+ _ => listValueTypes.NestedListValType.Value | DMValueType.Null
+ };
+ }
}
public sealed class CallOperation : NamedOperation {
@@ -73,7 +85,8 @@ private DMComplexValueType DetermineValType() {
while (!type.IsAnything && i < _operations.Length) {
var operation = _operations[i++];
- if (type.TypePath is null || DMObjectTree.GetDMObject(type.TypePath.Value, false) is not { } dmObject) {
+ var typePath = type.TypePath ?? type.AsPath();
+ if (typePath is null || DMObjectTree.GetDMObject(typePath.Value, false) is not { } dmObject) {
// We're dereferencing something without a type-path, this could be anything
type = DMValueType.Anything;
break;
@@ -81,8 +94,8 @@ private DMComplexValueType DetermineValType() {
type = operation switch {
FieldOperation fieldOperation => dmObject.GetVariable(fieldOperation.Identifier)?.ValType ?? DMValueType.Anything,
- IndexOperation => DMValueType.Anything, // Lists currently can't be typed, this could be anything
- CallOperation callOperation => dmObject.GetProcReturnTypes(callOperation.Identifier) ?? DMValueType.Anything,
+ IndexOperation indexOperation => indexOperation.UnnestValType(type.ListValueTypes), // TODO: Keys of assoc lists
+ CallOperation callOperation => dmObject.GetProcReturnTypes(callOperation.Identifier, callOperation.Parameters) ?? DMValueType.Anything,
_ => throw new InvalidOperationException("Unimplemented dereference operation")
};
}
@@ -126,7 +139,15 @@ private void EmitOperation(DMObject dmObject, DMProc proc, Operation operation,
break;
case CallOperation callOperation:
- var (argumentsType, argumentStackSize) = callOperation.Parameters.EmitArguments(dmObject, proc, null);
+ DMProc? targetProc = null;
+ if (callOperation.Path is not null) {
+ var obj = DMObjectTree.GetDMObject(callOperation.Path.Value, false);
+ var procs = obj?.GetProcs(callOperation.Identifier);
+ if (procs is not null && procs.Count > 0) {
+ targetProc = DMObjectTree.AllProcs[procs[0]];
+ }
+ }
+ var (argumentsType, argumentStackSize) = callOperation.Parameters.EmitArguments(dmObject, proc, targetProc);
proc.DereferenceCall(callOperation.Identifier, argumentsType, argumentStackSize);
break;
@@ -329,6 +350,12 @@ internal sealed class ScopeReference(Location location, DMExpression expression,
])
) {
public override DreamPath? Path => Expression.Path;
+ public override DMComplexValueType ValType {
+ get {
+ TryAsConstant(out var constant);
+ return constant is not null ? constant.ValType : dmVar.ValType;
+ }
+ }
public override string GetNameof(DMObject dmObject) => dmVar.Name;
diff --git a/DMCompiler/DM/Expressions/LValue.cs b/DMCompiler/DM/Expressions/LValue.cs
index 57b4512dc6..792f81e021 100644
--- a/DMCompiler/DM/Expressions/LValue.cs
+++ b/DMCompiler/DM/Expressions/LValue.cs
@@ -72,11 +72,17 @@ public override DMReference EmitReference(DMObject dmObject, DMProc proc, string
}
// Identifier of local variable
-internal sealed class Local(Location location, DMProc.LocalVariable localVar) : LValue(location, localVar.Type) {
+internal sealed class Local(Location location, DMProc.LocalVariable localVar, DMProc proc, DMComplexValueType? valType) : LValue(location, localVar.Type) {
public DMProc.LocalVariable LocalVar { get; } = localVar;
-
- // TODO: non-const local var static typing
- public override DMComplexValueType ValType => LocalVar.ExplicitValueType ?? DMValueType.Anything;
+ public override DMComplexValueType ValType {
+ get {
+ //todo: allow local variables to be param-typed again
+ // WITHOUT having to pass procParameters through the whole parser chain
+ //if (valType is not null) return proc.GetBaseProc().GetParameterValueTypes(valType.Value, null);
+ if (valType is not null) return valType.Value;
+ return LocalVar.Type is not null ? new DMComplexValueType(DMValueType.Instance | DMValueType.Path | DMValueType.Null, LocalVar.Type) : DMValueType.Anything;
+ }
+ }
public override DMReference EmitReference(DMObject dmObject, DMProc proc, string endLabel, ShortCircuitMode shortCircuitMode = ShortCircuitMode.KeepNull) {
if (LocalVar.IsParameter) {
diff --git a/DMCompiler/DM/Expressions/Procs.cs b/DMCompiler/DM/Expressions/Procs.cs
index 0daf5ba614..4ff8df6ec4 100644
--- a/DMCompiler/DM/Expressions/Procs.cs
+++ b/DMCompiler/DM/Expressions/Procs.cs
@@ -5,7 +5,10 @@
namespace DMCompiler.DM.Expressions;
// x() (only the identifier)
-internal sealed class Proc(Location location, string identifier) : DMExpression(location) {
+internal sealed class Proc(Location location, string identifier, DMObject theObject) : DMExpression(location) {
+ public DMObject dmObject => theObject;
+ public string Identifier => identifier;
+ public override DMComplexValueType ValType => GetReturnType(theObject);
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
DMCompiler.Emit(WarningCode.BadExpression, Location, "attempt to use proc as value");
proc.Error();
@@ -39,6 +42,7 @@ public DMComplexValueType GetReturnType(DMObject dmObject) {
///
internal sealed class GlobalProc(Location location, string name) : DMExpression(location) {
public override DMComplexValueType ValType => GetProc().ReturnTypes;
+ public string Identifier => name;
public override string ToString() {
return $"{name}()";
@@ -68,7 +72,7 @@ public DMProc GetProc() {
/// This is an LValue _and_ a proc!
///
internal sealed class ProcSelf(Location location, DreamPath? path, DMProc proc) : LValue(location, path) {
- public override DMComplexValueType ValType => proc.ReturnTypes;
+ public override DMComplexValueType ValType => proc.GetParameterValueTypes(null);
public override DMReference EmitReference(DMObject dmObject, DMProc proc, string endLabel, ShortCircuitMode shortCircuitMode = ShortCircuitMode.KeepNull) {
return DMReference.Self;
@@ -77,7 +81,7 @@ public override DMReference EmitReference(DMObject dmObject, DMProc proc, string
// ..
internal sealed class ProcSuper(Location location, DMObject _dmObject, DMProc _proc) : DMExpression(location) {
- public override DMComplexValueType ValType => _dmObject.GetProcReturnTypes(_proc.Name) ?? DMValueType.Anything;
+ public override DMComplexValueType ValType => _dmObject.GetProcReturnTypes(_proc.Name) ?? DMValueType.Anything; // TODO: could this just be _proc.ReturnTypes?
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
DMCompiler.Emit(WarningCode.InvalidReference, Location, $"Attempt to use proc \"..\" as value");
@@ -100,7 +104,21 @@ public override DMReference EmitReference(DMObject dmObject, DMProc proc, string
internal sealed class ProcCall(Location location, DMExpression target, ArgumentList arguments, DMComplexValueType valType)
: DMExpression(location) {
public override bool PathIsFuzzy => Path == null;
- public override DMComplexValueType ValType => valType.IsAnything ? target.ValType : valType;
+ public override DMComplexValueType ValType {
+ get {
+ if (!valType.IsAnything)
+ return valType;
+ switch (target) {
+ case Proc procTarget:
+ return procTarget.dmObject.GetProcReturnTypes(procTarget.Identifier, arguments) ?? DMValueType.Anything;
+ case GlobalProc procTarget:
+ if(DMObjectTree.TryGetGlobalProc(procTarget.Identifier, out var globalProc))
+ return globalProc.GetParameterValueTypes(arguments);
+ return DMValueType.Anything;
+ }
+ return target.ValType;
+ }
+ }
public (DMObject? ProcOwner, DMProc? Proc) GetTargetProc(DMObject dmObject) {
return target switch {
diff --git a/DMCompiler/DM/Expressions/Ternary.cs b/DMCompiler/DM/Expressions/Ternary.cs
index 27bc106657..1af236c108 100644
--- a/DMCompiler/DM/Expressions/Ternary.cs
+++ b/DMCompiler/DM/Expressions/Ternary.cs
@@ -17,10 +17,13 @@ public Ternary(Location location, DMExpression a, DMExpression b, DMExpression c
if (b.ValType.TypePath != null && c.ValType.TypePath != null && b.ValType.TypePath != c.ValType.TypePath) {
DMCompiler.Emit(WarningCode.LostTypeInfo, Location,
- $"Ternary has type paths {b.ValType.TypePath} and {c.ValType.TypePath} but a value can only have one type path. Using {b.ValType.TypePath}.");
+ $"Ternary has type paths {b.ValType.TypePath} and {c.ValType.TypePath} but a value can only have one type path. Using their common ancestor, {b.ValType.TypePath.Value.GetLastCommonAncestor(c.ValType.TypePath.Value)}.");
}
- ValType = new(b.ValType.Type | c.ValType.Type, b.ValType.TypePath ?? c.ValType.TypePath);
+ if (b.ValType.IsAnything || c.ValType.IsAnything)
+ ValType = DMValueType.Anything;
+ else
+ ValType = b.ValType | c.ValType;
}
public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) {
diff --git a/DMCompiler/DM/Expressions/Unary.cs b/DMCompiler/DM/Expressions/Unary.cs
index 3f53b2f716..b7c3c3edb7 100644
--- a/DMCompiler/DM/Expressions/Unary.cs
+++ b/DMCompiler/DM/Expressions/Unary.cs
@@ -9,6 +9,7 @@ internal abstract class UnaryOp(Location location, DMExpression expr) : DMExpres
// -x
internal sealed class Negate(Location location, DMExpression expr) : UnaryOp(location, expr) {
+ public override DMComplexValueType ValType => Expr.ValType; // could be a num, could be a matrix, you never know
public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) {
if (!Expr.TryAsConstant(out constant) || constant is not Number number)
return false;
@@ -25,6 +26,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// !x
internal sealed class Not(Location location, DMExpression expr) : UnaryOp(location, expr) {
+ public override DMComplexValueType ValType => DMValueType.Num; // could eventually be updated to be a bool
public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) {
if (!Expr.TryAsConstant(out constant)) return false;
@@ -40,6 +42,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
// ~x
internal sealed class BinaryNot(Location location, DMExpression expr) : UnaryOp(location, expr) {
+ public override DMComplexValueType ValType => DMValueType.Num;
public override bool TryAsConstant([NotNullWhen(true)] out Constant? constant) {
if (!Expr.TryAsConstant(out constant) || constant is not Number constantNum)
return false;
@@ -55,6 +58,7 @@ public override void EmitPushValue(DMObject dmObject, DMProc proc) {
}
internal abstract class AssignmentUnaryOp(Location location, DMExpression expr) : UnaryOp(location, expr) {
+ public override DMComplexValueType ValType => Expr.ValType;
protected abstract void EmitOp(DMObject dmObject, DMProc proc, DMReference reference, string endLabel);
public override void EmitPushValue(DMObject dmObject, DMProc proc) {
diff --git a/DMCompiler/DMStandard/Types/Atoms/Mob.dm b/DMCompiler/DMStandard/Types/Atoms/Mob.dm
index d8550a7728..6433e62340 100644
--- a/DMCompiler/DMStandard/Types/Atoms/Mob.dm
+++ b/DMCompiler/DMStandard/Types/Atoms/Mob.dm
@@ -1,16 +1,16 @@
/mob
parent_type = /atom/movable
- var/client/client
+ var/client/client as /client|null
var/key as text|null
var/tmp/ckey as text|null
var/tmp/list/group as opendream_unimplemented
- var/see_invisible = 0
- var/see_infrared = 0 as opendream_unimplemented
- var/sight = 0
- var/see_in_dark = 2 as opendream_unimplemented
+ var/see_invisible = 0 as num
+ var/see_infrared = 0 as num|opendream_unimplemented
+ var/sight = 0 as num
+ var/see_in_dark = 2 as num|opendream_unimplemented
density = TRUE
layer = MOB_LAYER
diff --git a/DMCompiler/DMStandard/Types/Atoms/Movable.dm b/DMCompiler/DMStandard/Types/Atoms/Movable.dm
index 185e87280f..e37d3ef7ee 100644
--- a/DMCompiler/DMStandard/Types/Atoms/Movable.dm
+++ b/DMCompiler/DMStandard/Types/Atoms/Movable.dm
@@ -1,9 +1,9 @@
/atom/movable
- var/screen_loc
+ var/screen_loc as text|null
var/animate_movement = FORWARD_STEPS as opendream_unimplemented
var/list/locs = null as opendream_unimplemented
- var/glide_size = 0
+ var/glide_size = 0 as num
var/step_size as opendream_unimplemented
var/tmp/bound_x as opendream_unimplemented
var/tmp/bound_y as opendream_unimplemented
@@ -17,7 +17,7 @@
proc/Bump(atom/Obstacle)
- proc/Move(atom/NewLoc, Dir=0) as num
+ proc/Move(atom/NewLoc, Dir=0 as num)
if (isnull(NewLoc) || loc == NewLoc)
return FALSE
@@ -57,5 +57,4 @@
newarea.Entered(src, oldloc)
return TRUE
- else
- return FALSE
+ return FALSE
diff --git a/DMCompiler/DMStandard/Types/Atoms/_Atom.dm b/DMCompiler/DMStandard/Types/Atoms/_Atom.dm
index 9ce5c003d2..96699f6bbc 100644
--- a/DMCompiler/DMStandard/Types/Atoms/_Atom.dm
+++ b/DMCompiler/DMStandard/Types/Atoms/_Atom.dm
@@ -1,10 +1,10 @@
/atom
parent_type = /datum
- var/name = null
- var/text = null
- var/desc = null
- var/suffix = null as opendream_unimplemented
+ var/name = null as text|null
+ var/text = null as text|null
+ var/desc = null as text|null
+ var/suffix = null as text|null|opendream_unimplemented
// The initialization/usage of these lists is handled internally by the runtime
var/tmp/list/verbs = null
@@ -14,8 +14,8 @@
var/tmp/list/vis_locs = null as opendream_unimplemented
var/list/vis_contents = null
- var/tmp/atom/loc
- var/dir = SOUTH
+ var/tmp/atom/loc as /atom|null
+ var/dir = SOUTH as num
var/tmp/x = 0
var/tmp/y = 0
var/tmp/z = 0
@@ -26,20 +26,23 @@
var/icon = null
var/icon_state = ""
- var/layer = 2.0
- var/plane = 0
- var/alpha = 255
- var/color = "#FFFFFF"
- var/invisibility = 0
- var/mouse_opacity = 1
- var/infra_luminosity = 0 as opendream_unimplemented
- var/luminosity = 0 as opendream_unimplemented
- var/opacity = 0
+ var/layer = 2.0 as num
+ var/plane = 0 as num
+ var/alpha = 255 as num
+ // currently we coerce text (hex string) and list(num) (color matrix) to the color type
+ // in the future this should probably be colorstring|colormatrix|null or something
+ // or 'as color' could be a shorthand for that
+ var/color = "#FFFFFF" as color|null
+ var/invisibility = 0 as num
+ var/mouse_opacity = 1 as num
+ var/infra_luminosity = 0 as num|opendream_unimplemented
+ var/luminosity = 0 as num|opendream_unimplemented
+ var/opacity = 0 as num
var/matrix/transform
- var/blend_mode = 0
+ var/blend_mode = 0 as num
var/gender = NEUTER
- var/density = FALSE
+ var/density = FALSE as num
var/maptext as opendream_unimplemented
diff --git a/DMCompiler/DMStandard/Types/Client.dm b/DMCompiler/DMStandard/Types/Client.dm
index 041b34e8aa..b0856ad03d 100644
--- a/DMCompiler/DMStandard/Types/Client.dm
+++ b/DMCompiler/DMStandard/Types/Client.dm
@@ -11,7 +11,7 @@
var/tag
var/const/type = /client
- var/mob/mob as /mob|null
+ var/mob/mob as mob|null
var/atom/eye
var/lazy_eye = 0 as opendream_unimplemented
var/perspective = MOB_PERSPECTIVE
@@ -49,8 +49,7 @@
proc/New(TopicData)
// Search every mob for one with our ckey
- // TODO: This /mob|mob thing is kinda silly huh?
- for (var/mob/M as /mob|mob in world)
+ for (var/mob/M as mob in world)
if (M.key == key)
mob = M
break
@@ -89,7 +88,7 @@
proc/MeasureText(text, style, width=0)
set opendream_unimplemented = TRUE
- proc/Move(loc, dir)
+ proc/Move(loc, dir as num)
mob.Move(loc, dir)
proc/North()
diff --git a/DMCompiler/DMStandard/Types/Datum.dm b/DMCompiler/DMStandard/Types/Datum.dm
index c14bfb2a23..96e33e8744 100644
--- a/DMCompiler/DMStandard/Types/Datum.dm
+++ b/DMCompiler/DMStandard/Types/Datum.dm
@@ -1,5 +1,5 @@
/datum
- var/tmp/type as opendream_compiletimereadonly
+ var/tmp/type as path(/datum)|opendream_compiletimereadonly
var/tmp/parent_type
var/tmp/list/vars as opendream_compiletimereadonly
diff --git a/DMCompiler/DMStandard/Types/Icon.dm b/DMCompiler/DMStandard/Types/Icon.dm
index bd211b7de7..806ca704c6 100644
--- a/DMCompiler/DMStandard/Types/Icon.dm
+++ b/DMCompiler/DMStandard/Types/Icon.dm
@@ -44,5 +44,5 @@
proc/Width()
-proc/icon(icon, icon_state, dir, frame, moving)
+proc/icon(icon, icon_state, dir, frame, moving) as icon
return new /icon(icon, icon_state, dir, frame, moving)
diff --git a/DMCompiler/DMStandard/Types/List.dm b/DMCompiler/DMStandard/Types/List.dm
index 013c1e56c3..96226ad55a 100644
--- a/DMCompiler/DMStandard/Types/List.dm
+++ b/DMCompiler/DMStandard/Types/List.dm
@@ -4,15 +4,15 @@
proc/New(Size)
- proc/Add(Item1)
- proc/Copy(Start = 1, End = 0)
- proc/Cut(Start = 1, End = 0)
- proc/Find(Elem, Start = 1, End = 0)
- proc/Insert(Index, Item1)
+ proc/Add(Item1) as null
+ proc/Copy(Start = 1, End = 0) as /list
+ proc/Cut(Start = 1, End = 0) as num
+ proc/Find(Elem, Start = 1, End = 0) as num
+ proc/Insert(Index, Item1) as num
proc/Join(Glue as text|null, Start = 1 as num, End = 0 as num) as text
- proc/Remove(Item1)
- proc/RemoveAll(Item1)
- proc/Swap(Index1, Index2)
+ proc/Remove(Item1) as num
+ proc/RemoveAll(Item1) as num
+ proc/Swap(Index1, Index2) as null
- proc/Splice(Start=1,End=0, ...)
+ proc/Splice(Start=1,End=0, ...) as null
set opendream_unimplemented = TRUE
diff --git a/DMCompiler/DMStandard/Types/World.dm b/DMCompiler/DMStandard/Types/World.dm
index 39f7242d4c..61a41d0b72 100644
--- a/DMCompiler/DMStandard/Types/World.dm
+++ b/DMCompiler/DMStandard/Types/World.dm
@@ -4,20 +4,20 @@
var/log = null
- var/area = /area as /area
- var/turf = /turf as /turf
- var/mob = /mob as /mob
+ var/area = /area as path(/area)
+ var/turf = /turf as path(/turf)
+ var/mob = /mob as path(/mob)
var/name = "OpenDream World"
- var/time
- var/timezone = 0
- var/timeofday
- var/realtime
- var/tick_lag = 1
- var/cpu = 0 as opendream_unimplemented
- var/fps = 10
+ var/time as num
+ var/timezone = 0 as num
+ var/timeofday as num
+ var/realtime as num
+ var/tick_lag = 1 as num
+ var/cpu = 0 as opendream_unimplemented|num
+ var/fps = 10 as num
var/tick_usage
- var/loop_checks = 0 as opendream_unimplemented
+ var/loop_checks = 0 as opendream_unimplemented|num
var/maxx = null as num|null
var/maxy = null as num|null
@@ -32,7 +32,7 @@
var/version = 0 as opendream_unimplemented
var/address
- var/port = 0 as opendream_compiletimereadonly
+ var/port = 0 as opendream_compiletimereadonly|num
var/internet_address = "127.0.0.1" as opendream_unimplemented
var/url as opendream_unimplemented
var/visibility = 0 as opendream_unimplemented
@@ -40,7 +40,7 @@
var/process
var/list/params = null
- var/sleep_offline = 0
+ var/sleep_offline = 0 as num
var/system_type
diff --git a/DMCompiler/DMStandard/_Standard.dm b/DMCompiler/DMStandard/_Standard.dm
index efa069e43d..865c984ab5 100644
--- a/DMCompiler/DMStandard/_Standard.dm
+++ b/DMCompiler/DMStandard/_Standard.dm
@@ -1,17 +1,17 @@
-/var/world/world = /world as /world // Set to /world to suppress issues with Typemaker
+/var/world/world = /world as /world|path(/world)
//These procs should be in alphabetical order, as in DreamProcNativeRoot.cs
proc/alert(Usr = usr, Message, Title, Button1 = "Ok", Button2, Button3) as text
proc/animate(Object, time, loop, easing, flags, delay, pixel_x, pixel_y, pixel_z, maptext, maptext_width, maptext_height, maptext_x, maptext_y, dir, alpha, transform, color, luminosity, infra_luminosity, layer, glide_size, icon, icon_state, invisibility, suffix) as null
-proc/ascii2text(N) as text
-proc/block(atom/Start, atom/End, StartZ, EndX=Start, EndY=End, EndZ=StartZ) as /list
-proc/ceil(A) as num
-proc/ckey(Key) as text|null
+proc/ascii2text(N as num|null) as text
+proc/block(atom/Start, atom/End, StartZ, EndX=Start, EndY=End, EndZ=StartZ) as /list(/turf)
+proc/ceil(A as num|null) as num
+proc/ckey(Key as text|null) as text|null
proc/ckeyEx(Text) as text|null
proc/clamp(Value, Low, High) as /list|num|null
proc/cmptext(T1) as num
-proc/copytext(T, Start = 1, End = 0) as text|null
-proc/copytext_char(T,Start=1,End=0) as text|null
+proc/copytext(T as text|null, Start = 1, End = 0) as text|null
+proc/copytext_char(T as text|null,Start=1,End=0) as text|null
proc/CRASH(msg) as null
proc/fcopy(Src, Dst) as num
proc/fcopy_rsc(File) as num|null
@@ -20,10 +20,10 @@ proc/fexists(File) as num
proc/file(Path)
proc/file2text(File) as text|null
proc/filter(type, ...)
-proc/findtext(Haystack, Needle, Start = 1, End = 0) as num
-proc/findtextEx(Haystack, Needle, Start = 1, End = 0) as num
-proc/findlasttext(Haystack, Needle, Start = 1, End = 0) as num
-proc/findlasttextEx(Haystack, Needle, Start = 1, End = 0) as num
+proc/findtext(Haystack as text|null, Needle as text|/regex|null, Start = 1, End = 0) as num
+proc/findtextEx(Haystack as text|null, Needle as text|/regex|null, Start = 1, End = 0) as num
+proc/findlasttext(Haystack as text|null, Needle as text|/regex|null, Start = 1, End = 0) as num
+proc/findlasttextEx(Haystack as text|null, Needle as text|/regex|null, Start = 1, End = 0) as num
proc/flick(Icon, Object)
proc/flist(Path) as /list
proc/floor(A) as num
@@ -35,7 +35,7 @@ proc/hearers(Depth = world.view, Center = usr) as /list
proc/html_decode(HtmlText) as text
proc/html_encode(PlainText) as text
proc/icon_states(Icon, mode = 0) as text|null
-proc/image(icon, loc, icon_state, layer, dir, pixel_x, pixel_y)
+proc/image(icon, loc, icon_state, layer, dir, pixel_x, pixel_y) as /image
proc/isarea(Loc1) as num
proc/isfile(File) as num
proc/isicon(Icon) as num
@@ -55,7 +55,7 @@ proc/json_decode(JSON)
proc/json_encode(Value, flags)
proc/length_char(E) as num
proc/list2params(List) as text
-proc/lowertext(T as text) as text
+proc/lowertext(T as text|null) as text
proc/max(A) as num|text|null
proc/md5(T) as text|null
proc/min(A) as num|text|null
@@ -63,25 +63,25 @@ proc/noise_hash(...) as num
set opendream_unimplemented = 1
return 0.5
proc/nonspantext(Haystack, Needles, Start = 1) as num
-proc/num2text(N, A, B) as text
+proc/num2text(N as num|null, A, B) as text
proc/orange(Dist = 5, Center = usr) as /list|null // NOTE: Not sure if return types have BYOND parity
proc/oview(Dist = 5, Center = usr) as /list
proc/oviewers(Depth = 5, Center = usr) as /list
proc/ohearers(Depth = world.view, Center = usr) as /list
-proc/params2list(Params) as /list
-proc/rand(L, H) as num
+proc/params2list(Params) as /list(text)
+proc/rand(L as num, H as num) as num
proc/rand_seed(Seed) as null
proc/range(Dist, Center) as /list|null // NOTE: Not sure if return types have BYOND parity
proc/ref(Object) as text
-proc/replacetext(Haystack, Needle, Replacement, Start = 1, End = 0) as text|null
-proc/replacetextEx(Haystack, Needle, Replacement, Start = 1, End = 0) as text|null
-proc/rgb(R, G, B, A, space) as text|null
+proc/replacetext(Haystack as text|null, Needle as text|/regex|null, Replacement, Start = 1, End = 0) as text|null
+proc/replacetextEx(Haystack as text|null, Needle as text|/regex|null, Replacement, Start = 1, End = 0) as text|null
+proc/rgb(R, G, B, A, space) as color|null
proc/rgb2num(color, space = COLORSPACE_RGB) as /list
-proc/roll(ndice = 1, sides) as num
-proc/round(A, B) as num
+proc/roll(ndice = 1 as num|text, sides as num|null) as num
+proc/round(A as num|null, B as num|null) as num
proc/sha1(input) as text|null
proc/shutdown(Addr,Natural = 0)
-proc/sleep(Delay)
+proc/sleep(Delay as num|null)
proc/sorttext(T1, T2) as num
proc/sorttextEx(T1, T2) as num
proc/sound(file, repeat = 0, wait, channel, volume)
@@ -89,19 +89,19 @@ proc/spantext(Haystack,Needles,Start=1) as num
proc/spantext_char(Haystack,Needles,Start=1) as num
proc/splicetext(Text, Start = 1, End = 0, Insert = "") as text|null
proc/splicetext_char(Text, Start = 1, End = 0, Insert = "") as text|null
-proc/splittext(Text, Delimiter) as /list
+proc/splittext(Text, Delimiter) as /list(text)
proc/stat(Name, Value)
proc/statpanel(Panel, Name, Value)
-proc/text2ascii(T, pos = 1) as text
-proc/text2ascii_char(T, pos = 1) as text
+proc/text2ascii(T as text|null, pos = 1) as num
+proc/text2ascii_char(T as text|null, pos = 1) as num
proc/text2file(Text, File)
proc/text2num(T, radix = 10) as num|null
-proc/text2path(T)
+proc/text2path(T as text|null) as null|path(/datum)|path(/world) // todo: allow path(/)
proc/time2text(timestamp, format) as text
proc/trimtext(Text) as text|null
-proc/trunc(n) as num
-proc/turn(Dir, Angle)
-proc/typesof(Item1) as /list
+proc/trunc(n as num|null) as num
+proc/turn(Dir as null|num|/matrix|icon, Angle as num) as null|num|/matrix|icon
+proc/typesof(Item1 as text|path(/datum)|path(/proc)|/list(text|path(/datum)|path(/proc))) as /list(path(/datum)|path(/proc))
proc/uppertext(T as text) as text
proc/url_decode(UrlText) as text
proc/url_encode(PlainText, format = 0) as text
@@ -142,35 +142,35 @@ proc/winset(player, control_id, params)
#include "Types\Atoms\Turf.dm"
#include "UnsortedAdditions.dm"
-proc/replacetextEx_char(Haystack as text, Needle, Replacement, Start = 1, End = 0) as text
+proc/replacetextEx_char(Haystack as text, Needle, Replacement, Start = 1 as num, End = 0 as num) as text
set opendream_unimplemented = TRUE
return Haystack
-/proc/step(atom/movable/Ref as /atom/movable, var/Dir, var/Speed=0) as num
+/proc/step(atom/movable/Ref, var/Dir as num|null, var/Speed=0 as num) as num
//TODO: Speed = step_size if Speed is 0
return Ref.Move(get_step(Ref, Dir), Dir)
-/proc/step_away(atom/movable/Ref as /atom/movable, /atom/Trg, Max=5, Speed=0) as num
+/proc/step_away(atom/movable/Ref, atom/Trg, Max=5 as num, Speed=0 as num) as num
return Ref.Move(get_step_away(Ref, Trg, Max), turn(get_dir(Ref, Trg), 180))
-/proc/step_to(atom/movable/Ref, atom/Trg, Min = 0, Speed = 0) as num
+/proc/step_to(atom/movable/Ref, atom/Trg, Min = 0 as num, Speed = 0 as num) as num
//TODO: Consider obstacles
var/dist = get_dist(Ref, Trg)
- if (dist <= Min) return
+ if (dist <= Min) return 0
- var/step_dir = get_dir(Ref, Trg)
+ var/step_dir = get_dir(Ref, Trg) as num
return step(Ref, step_dir, Speed)
-/proc/get_step_to(Ref, Trg, Min=0)
+/proc/get_step_to(Ref, Trg, Min=0 as num)
set opendream_unimplemented = TRUE
CRASH("/get_step_to() is not implemented")
-/proc/get_steps_to(Ref, Trg, Min=0) as /list
+/proc/get_steps_to(Ref, Trg, Min=0 as num)
set opendream_unimplemented = TRUE
CRASH("/get_steps_to() is not implemented")
-/proc/walk_away(Ref,Trg,Max=5,Lag=0,Speed=0)
+/proc/walk_away(Ref,Trg,Max=5 as num,Lag=0 as num,Speed=0 as num)
set opendream_unimplemented = TRUE
CRASH("/walk_away() is not implemented")
@@ -182,12 +182,12 @@ proc/get_dist(atom/Loc1, atom/Loc2) as num
var/distY = Loc2.y - Loc1.y
return round(sqrt(distX ** 2 + distY ** 2))
-proc/get_step_towards(atom/movable/Ref, /atom/Trg)
+proc/get_step_towards(atom/movable/Ref, atom/Trg)
var/dir = get_dir(Ref, Trg)
return get_step(Ref, dir)
-proc/get_step_away(atom/movable/Ref, /atom/Trg, Max = 5)
+proc/get_step_away(atom/movable/Ref, atom/Trg, Max = 5 as num)
var/dir = turn(get_dir(Ref, Trg), 180)
return get_step(Ref, dir)
@@ -198,7 +198,7 @@ proc/get_step_rand(atom/movable/Ref)
return get_step(Ref, dir)
-proc/step_towards(atom/movable/Ref as /atom/movable, /atom/Trg, Speed) as num
+proc/step_towards(atom/movable/Ref, atom/Trg, Speed) as num
return Ref.Move(get_step_towards(Ref, Trg), get_dir(Ref, Trg))
proc/step_rand(atom/movable/Ref, Speed=0)
@@ -208,9 +208,7 @@ proc/step_rand(atom/movable/Ref, Speed=0)
proc/jointext(list/List as /list|text, Glue as text|null, Start = 1 as num, End = 0 as num) as text
if(islist(List))
return List.Join(Glue, Start, End)
- if(istext(List))
- return List
- CRASH("jointext was passed a non-list, non-text value")
+ return List
proc/lentext(T) as num
return length(T)
diff --git a/DMCompiler/DreamPath.cs b/DMCompiler/DreamPath.cs
index d8182693af..33785e7741 100644
--- a/DMCompiler/DreamPath.cs
+++ b/DMCompiler/DreamPath.cs
@@ -107,6 +107,14 @@ public DMValueType GetAtomType() {
return DMValueType.Turf;
if (dmType.IsSubtypeOf(Area))
return DMValueType.Area;
+ if (dmType.IsSubtypeOf(Movable))
+ return DMValueType.Mob | DMValueType.Obj;
+ if (dmType.IsSubtypeOf(Atom))
+ return DMValueType.Area | DMValueType.Turf | DMValueType.Obj | DMValueType.Mob;
+ if (dmType.IsSubtypeOf(Icon))
+ return DMValueType.Icon;
+ if (dmType.IsSubtypeOf(Sound))
+ return DMValueType.Sound;
return DMValueType.Anything;
}
@@ -257,4 +265,12 @@ private void Normalize(bool canHaveEmptyEntries) {
Elements = _elements[..writeIdx];
}
+
+ public DreamPath GetLastCommonAncestor(DreamPath other) {
+ var thisObject = DMObjectTree.GetDMObject(this, true);
+ var otherObject = DMObjectTree.GetDMObject(other, true);
+ if (thisObject is null || otherObject is null)
+ return Root;
+ return thisObject.GetLastCommonAncestor(otherObject);
+ }
}
diff --git a/DMCompiler/Location.cs b/DMCompiler/Location.cs
index e4895b167a..cf6fecfdf7 100644
--- a/DMCompiler/Location.cs
+++ b/DMCompiler/Location.cs
@@ -1,3 +1,4 @@
+using System.IO;
using System.Text;
namespace DMCompiler;
@@ -7,7 +8,7 @@ public readonly struct Location(string filePath, int? line, int? column) {
/// For when DM location information can't be determined.
///
public static readonly Location Unknown = new();
-
+
///
/// For when internal OpenDream warnings/errors are raised or something internal needs to be passed a location.
///
@@ -18,7 +19,7 @@ public readonly struct Location(string filePath, int? line, int? column) {
public int? Column { get; } = column;
public override string ToString() {
- var builder = new StringBuilder(SourceFile ?? "");
+ var builder = new StringBuilder((SourceFile is null) ? "" : Path.GetRelativePath(".", SourceFile));
if (Line is not null) {
builder.Append(":" + Line);