diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index cdc9c261c4..e4d9503b32 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -3,9 +3,11 @@ name: Lint on: pull_request: branches: [master] + types: [opened, synchronize, reopened, ready_for_review] jobs: lint: + if: github.event.pull_request.draft == false runs-on: ubuntu-latest steps: diff --git a/Content.Tests/DMProject/Broken Tests/Procs/Arglist/initial.dm b/Content.Tests/DMProject/Broken Tests/Procs/Arglist/initial.dm deleted file mode 100644 index 2e77abf518..0000000000 --- a/Content.Tests/DMProject/Broken Tests/Procs/Arglist/initial.dm +++ /dev/null @@ -1,6 +0,0 @@ - -/proc/_initial(...) - return initial(arglist(args)) - -/proc/RunTest() - return \ No newline at end of file diff --git a/Content.Tests/DMProject/Tests/Builtins/__IMPLIED_TYPE__.dm b/Content.Tests/DMProject/Tests/Builtins/__IMPLIED_TYPE__.dm index 97b3ef4fee..128041a6ce 100644 --- a/Content.Tests/DMProject/Tests/Builtins/__IMPLIED_TYPE__.dm +++ b/Content.Tests/DMProject/Tests/Builtins/__IMPLIED_TYPE__.dm @@ -1,7 +1,10 @@ -/datum/test/var/bar = "foobar" +/datum/test /proc/RunTest() var/datum/test/D = __IMPLIED_TYPE__ - ASSERT(D.bar == "foobar") + ASSERT(D == /datum/test) + D = ArgumentTest(__IMPLIED_TYPE__) +/proc/ArgumentTest(some_argument) + ASSERT(some_argument == /datum/test) \ No newline at end of file diff --git a/Content.Tests/DMProject/Broken Tests/List/ListNullArg.dm b/Content.Tests/DMProject/Tests/List/ListNullArg.dm similarity index 64% rename from Content.Tests/DMProject/Broken Tests/List/ListNullArg.dm rename to Content.Tests/DMProject/Tests/List/ListNullArg.dm index 082351cda2..ccbb0c7643 100644 --- a/Content.Tests/DMProject/Broken Tests/List/ListNullArg.dm +++ b/Content.Tests/DMProject/Tests/List/ListNullArg.dm @@ -1,7 +1,7 @@ // RUNTIME ERROR /proc/ListNullArg2(a[5][3]) - ASSERT(a[1].len == 3) + ASSERT(a[1].len == 3) // a should be null /proc/RunTest() ListNullArg2() diff --git a/Content.Tests/DMProject/Broken Tests/List/ListNullArg1.dm b/Content.Tests/DMProject/Tests/List/ListNullArg1.dm similarity index 65% rename from Content.Tests/DMProject/Broken Tests/List/ListNullArg1.dm rename to Content.Tests/DMProject/Tests/List/ListNullArg1.dm index ade75cf317..9b8360c394 100644 --- a/Content.Tests/DMProject/Broken Tests/List/ListNullArg1.dm +++ b/Content.Tests/DMProject/Tests/List/ListNullArg1.dm @@ -1,7 +1,7 @@ // RUNTIME ERROR /proc/ListNullArg1(a[5]) - ASSERT(a.len == 5) + ASSERT(a.len == 5) // a should be null /proc/RunTest() ListNullArg1() diff --git a/Content.Tests/DMProject/Tests/Procs/Arglist/initial.dm b/Content.Tests/DMProject/Tests/Procs/Arglist/initial.dm new file mode 100644 index 0000000000..716becd5d4 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Procs/Arglist/initial.dm @@ -0,0 +1,6 @@ + +/proc/_initial(...) + ASSERT(initial(arglist(args))[1] == "foo") + +/proc/RunTest() + _initial("foo") diff --git a/Content.Tests/DMProject/Broken Tests/Statements/For/reuse_decl_const.dm b/Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const.dm similarity index 89% rename from Content.Tests/DMProject/Broken Tests/Statements/For/reuse_decl_const.dm rename to Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const.dm index 4b404cb05c..2f39dc4f12 100644 --- a/Content.Tests/DMProject/Broken Tests/Statements/For/reuse_decl_const.dm +++ b/Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const.dm @@ -1,4 +1,4 @@ -// COMPILE ERROR +// COMPILE ERROR OD0501 /datum var/const/idx = 0 diff --git a/Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const2.dm b/Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const2.dm new file mode 100644 index 0000000000..a8144a4359 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Statements/For/reuse_decl_const2.dm @@ -0,0 +1,18 @@ +// COMPILE ERROR OD0501 + +/datum + var/const/idx = 0 + var/c = 0 + proc/do_loop() + for (idx in list(1,2,3)) + c += idx + +/proc/RunTest() + var/datum/d = new + d.do_loop() + + var/const/idx = 0 + var/c = 0 + for (idx in list(1,2,3)) + c += idx + diff --git a/Content.Tests/DMProject/Tests/Statements/extra_token_pragma.dm b/Content.Tests/DMProject/Tests/Statements/extra_token_pragma.dm new file mode 100644 index 0000000000..ce54b039e4 --- /dev/null +++ b/Content.Tests/DMProject/Tests/Statements/extra_token_pragma.dm @@ -0,0 +1,6 @@ +// COMPILE ERROR OD3205 +#pragma ExtraToken error + +/proc/RunTest() + if(1). + ASSERT(TRUE) diff --git a/Content.Tests/DMProject/Broken Tests/Statements/If/thing_after_if.dm b/Content.Tests/DMProject/Tests/Statements/thing_after_statement.dm similarity index 58% rename from Content.Tests/DMProject/Broken Tests/Statements/If/thing_after_if.dm rename to Content.Tests/DMProject/Tests/Statements/thing_after_statement.dm index cd943fa17f..194f1ab340 100644 --- a/Content.Tests/DMProject/Broken Tests/Statements/If/thing_after_if.dm +++ b/Content.Tests/DMProject/Tests/Statements/thing_after_statement.dm @@ -8,3 +8,7 @@ ASSERT(TRUE) else ASSERT(FALSE) + for(var/i in 1 to 1): + ASSERT(TRUE) + for(var/i in 1 to 1). + ASSERT(TRUE) diff --git a/Content.Tests/DMProject/Broken Tests/Stdlib/Array/arg.dm b/Content.Tests/DMProject/Tests/Stdlib/Array/arg.dm similarity index 100% rename from Content.Tests/DMProject/Broken Tests/Stdlib/Array/arg.dm rename to Content.Tests/DMProject/Tests/Stdlib/Array/arg.dm diff --git a/DMCompiler/Bytecode/DreamProcOpcode.cs b/DMCompiler/Bytecode/DreamProcOpcode.cs index 3d5d36dfaa..054a27b89c 100644 --- a/DMCompiler/Bytecode/DreamProcOpcode.cs +++ b/DMCompiler/Bytecode/DreamProcOpcode.cs @@ -295,6 +295,8 @@ public enum DreamProcOpcode : byte { ReturnReferenceValue = 0x97, [OpcodeMetadata(0, OpcodeArgType.Float)] ReturnFloat = 0x98, + [OpcodeMetadata(1, OpcodeArgType.Reference, OpcodeArgType.String)] + IndexRefWithString = 0x99, } // ReSharper restore MissingBlankLines diff --git a/DMCompiler/Compiler/CompilerError.cs b/DMCompiler/Compiler/CompilerError.cs index f23dc9a2df..b2752df1ce 100644 --- a/DMCompiler/Compiler/CompilerError.cs +++ b/DMCompiler/Compiler/CompilerError.cs @@ -74,6 +74,7 @@ public enum WarningCode { AssignmentInConditional = 3202, PickWeightedSyntax = 3203, AmbiguousInOrder = 3204, + ExtraToken = 3205, RuntimeSearchOperator = 3300, // 4000 - 4999 are reserved for runtime configuration. (TODO: Runtime doesn't know about configs yet!) diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index 00c566fd16..30a012d50e 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -1125,8 +1125,10 @@ private DMASTProcStatementIf If() { BracketWhitespace(); ConsumeRightParenthesis(); - Whitespace(); - Check(TokenType.DM_Colon); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + Whitespace(); DMASTProcStatement? procStatement = ProcStatement(); @@ -1165,6 +1167,10 @@ private DMASTProcStatement For() { Whitespace(); if (Check(TokenType.DM_RightParenthesis)) { + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementInfLoop(loc, GetForBody(loc)); } @@ -1185,6 +1191,10 @@ private DMASTProcStatement For() { if (expr1 is DMASTAssign assign) { ExpressionTo(out var endRange, out var step); Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after to expression"); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, new DMASTExpressionInRange(loc, assign.LHS, assign.RHS, endRange, step), null, null, dmTypes, GetForBody(loc)); } else { Emit(WarningCode.BadExpression, "Expected = before to in for"); @@ -1197,15 +1207,27 @@ private DMASTProcStatement For() { DMASTExpression? listExpr = Expression(); Whitespace(); Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 2"); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, new DMASTExpressionIn(loc, expr1, listExpr), null, null, dmTypes, GetForBody(loc)); } if (!Check(ForSeparatorTypes)) { Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 1"); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody(loc)); } if (Check(TokenType.DM_RightParenthesis)) { + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, expr1, null, null, dmTypes, GetForBody(loc)); } @@ -1221,10 +1243,18 @@ private DMASTProcStatement For() { if (!Check(ForSeparatorTypes)) { Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 2"); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody(loc)); } if (Check(TokenType.DM_RightParenthesis)) { + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, expr1, expr2, null, dmTypes, GetForBody(loc)); } @@ -1239,6 +1269,10 @@ private DMASTProcStatement For() { } Consume(TokenType.DM_RightParenthesis, "Expected ')' in for after expression 3"); + if (Check(TokenType.DM_Colon) || Check(TokenType.DM_Period)) { + Emit(WarningCode.ExtraToken, loc, "Extra token at end of proc statement"); + } + return new DMASTProcStatementFor(loc, expr1, expr2, expr3, dmTypes, GetForBody(loc)); DMASTProcBlockInner GetForBody(Location forLocation) { @@ -1690,7 +1724,9 @@ private List DefinitionParameters(out bool wasIndeterm var loc = Current().Location; Whitespace(); - DMASTExpression? value = PathArray(ref path.Path); + PathArray(ref path.Path); + + DMASTExpression? value = null; DMASTExpression? possibleValues = null; if (Check(TokenType.DM_DoubleSquareBracketEquals)) { diff --git a/DMCompiler/DM/Builders/DMExpressionBuilder.cs b/DMCompiler/DM/Builders/DMExpressionBuilder.cs index 6c72adc963..9e25dc50fe 100644 --- a/DMCompiler/DM/Builders/DMExpressionBuilder.cs +++ b/DMCompiler/DM/Builders/DMExpressionBuilder.cs @@ -75,12 +75,12 @@ private DMExpression BuildExpression(DMASTExpression expression, DreamPath? infe case DMASTDereference deref: result = BuildDereference(deref, inferredPath); break; case DMASTLocate locate: result = BuildLocate(locate, inferredPath); break; case DMASTImplicitIsType implicitIsType: result = BuildImplicitIsType(implicitIsType, inferredPath); break; - case DMASTList list: result = BuildList(list); break; + case DMASTList list: result = BuildList(list, inferredPath); break; case DMASTDimensionalList dimensionalList: result = BuildDimensionalList(dimensionalList, inferredPath); break; case DMASTNewList newList: result = BuildNewList(newList, inferredPath); break; case DMASTAddText addText: result = BuildAddText(addText, inferredPath); break; - case DMASTInput input: result = BuildInput(input); break; - case DMASTPick pick: result = BuildPick(pick); break; + case DMASTInput input: result = BuildInput(input, inferredPath); break; + case DMASTPick pick: result = BuildPick(pick, inferredPath); break; case DMASTLog log: result = BuildLog(log, inferredPath); break; case DMASTCall call: result = BuildCall(call, inferredPath); break; case DMASTExpressionWrapped wrapped: result = BuildExpression(wrapped.Value, inferredPath); break; @@ -327,10 +327,10 @@ private DMExpression BuildExpression(DMASTExpression expression, DreamPath? infe break; case DMASTGradient gradient: result = new Gradient(gradient.Location, - BuildArgumentList(gradient.Location, gradient.Parameters)); + BuildArgumentList(gradient.Location, gradient.Parameters, inferredPath)); break; case DMASTRgb rgb: - result = new Rgb(rgb.Location, BuildArgumentList(rgb.Location, rgb.Parameters)); + result = new Rgb(rgb.Location, BuildArgumentList(rgb.Location, rgb.Parameters, inferredPath)); break; case DMASTLocateCoordinates locateCoordinates: result = new LocateCoordinates(locateCoordinates.Location, @@ -435,7 +435,7 @@ private DMExpression BuildExpression(DMASTExpression expression, DreamPath? infe case DMASTVarDeclExpression varDeclExpr: var declIdentifier = new DMASTIdentifier(expression.Location, varDeclExpr.DeclPath.Path.LastElement); - result = BuildIdentifier(declIdentifier); + result = BuildIdentifier(declIdentifier, inferredPath); break; case DMASTVoid: result = BadExpression(WarningCode.BadExpression, expression.Location, "Attempt to use a void expression"); @@ -751,7 +751,7 @@ private DMExpression BuildProcCall(DMASTProcCall procCall, DreamPath? inferredPa } var target = BuildExpression((DMASTExpression)procCall.Callable, inferredPath); - var args = BuildArgumentList(procCall.Location, procCall.Parameters); + var args = BuildArgumentList(procCall.Location, procCall.Parameters, inferredPath); if (target is Proc targetProc) { // GlobalProc handles returnType itself var returnType = targetProc.GetReturnType(ctx.Type); @@ -885,7 +885,7 @@ private DMExpression BuildDereference(DMASTDereference deref, DreamPath? inferre return UnknownReference(callOperation.Location, $"Could not find a global proc named \"{callOperation.Identifier}\""); - var argumentList = BuildArgumentList(deref.Expression.Location, callOperation.Parameters); + var argumentList = BuildArgumentList(deref.Expression.Location, callOperation.Parameters, inferredPath); var globalProcExpr = new GlobalProc(expr.Location, globalProc); expr = new ProcCall(expr.Location, globalProcExpr, argumentList, DMValueType.Anything); @@ -1023,7 +1023,7 @@ private DMExpression BuildDereference(DMASTDereference deref, DreamPath? inferre case DMASTDereference.CallOperation callOperation: { var field = callOperation.Identifier; - var argumentList = BuildArgumentList(deref.Expression.Location, callOperation.Parameters); + var argumentList = BuildArgumentList(deref.Expression.Location, callOperation.Parameters, inferredPath); if (!callOperation.NoSearch && !pathIsFuzzy) { if (prevPath == null) { @@ -1081,7 +1081,7 @@ private DMExpression BuildImplicitIsType(DMASTImplicitIsType isType, DreamPath? return new IsTypeInferred(isType.Location, expr, expr.Path.Value); } - private DMExpression BuildList(DMASTList list) { + private DMExpression BuildList(DMASTList list, DreamPath? inferredPath) { (DMExpression? Key, DMExpression Value)[] values = []; if (list.Values != null) { @@ -1089,8 +1089,8 @@ private DMExpression BuildList(DMASTList list) { for (int i = 0; i < list.Values.Length; i++) { DMASTCallParameter value = list.Values[i]; - DMExpression? key = (value.Key != null) ? BuildExpression(value.Key) : null; - DMExpression listValue = BuildExpression(value.Value); + DMExpression? key = (value.Key != null) ? BuildExpression(value.Key, inferredPath) : null; + DMExpression listValue = BuildExpression(value.Value, inferredPath); values[i] = (key, listValue); } @@ -1151,7 +1151,7 @@ private DMExpression BuildAddText(DMASTAddText addText, DreamPath? inferredPath) return new AddText(addText.Location, expArr); } - private DMExpression BuildInput(DMASTInput input) { + private DMExpression BuildInput(DMASTInput input, DreamPath? inferredPath) { DMExpression[] arguments = new DMExpression[input.Parameters.Length]; for (int i = 0; i < input.Parameters.Length; i++) { DMASTCallParameter parameter = input.Parameters[i]; @@ -1161,12 +1161,12 @@ private DMExpression BuildInput(DMASTInput input) { "input() does not take named arguments"); } - arguments[i] = BuildExpression(parameter.Value); + arguments[i] = BuildExpression(parameter.Value, inferredPath); } DMExpression? list = null; if (input.List != null) { - list = BuildExpression(input.List); + list = BuildExpression(input.List, inferredPath); DMValueType objectTypes = DMValueType.Null |DMValueType.Obj | DMValueType.Mob | DMValueType.Turf | DMValueType.Area; @@ -1188,13 +1188,13 @@ private DMExpression BuildInput(DMASTInput input) { return new Input(input.Location, arguments, input.Types.Value, list); } - private DMExpression BuildPick(DMASTPick pick) { + private DMExpression BuildPick(DMASTPick pick, DreamPath? inferredPath) { Pick.PickValue[] pickValues = new Pick.PickValue[pick.Values.Length]; for (int i = 0; i < pickValues.Length; i++) { DMASTPick.PickValue pickValue = pick.Values[i]; - DMExpression? weight = (pickValue.Weight != null) ? BuildExpression(pickValue.Weight) : null; - DMExpression value = BuildExpression(pickValue.Value); + DMExpression? weight = (pickValue.Weight != null) ? BuildExpression(pickValue.Weight, inferredPath) : null; + DMExpression value = BuildExpression(pickValue.Value, inferredPath); if (weight is Prob prob) // pick(prob(50);x, prob(200);y) format weight = prob.P; diff --git a/DMCompiler/DM/Builders/DMProcBuilder.cs b/DMCompiler/DM/Builders/DMProcBuilder.cs index e4fcd52a2d..38f2c0d729 100644 --- a/DMCompiler/DM/Builders/DMProcBuilder.cs +++ b/DMCompiler/DM/Builders/DMProcBuilder.cs @@ -489,6 +489,10 @@ public void ProcessStatementFor(DMASTProcStatementFor statementFor) { var outputVar = _exprBuilder.Create(outputExpr); + if (outputVar is Local { LocalVar: DMProc.LocalConstVariable } or Field { IsConst: true }) { + compiler.Emit(WarningCode.WriteToConstant, outputExpr.Location, "Cannot change constant value"); + } + var start = _exprBuilder.Create(exprRange.StartRange); var end = _exprBuilder.Create(exprRange.EndRange); var step = exprRange.Step != null @@ -520,7 +524,10 @@ public void ProcessStatementFor(DMASTProcStatementFor statementFor) { if (outputVar is Local outputLocal) { outputLocal.LocalVar.ExplicitValueType = statementFor.DMTypes; - } + if(outputLocal.LocalVar is DMProc.LocalConstVariable) + compiler.Emit(WarningCode.WriteToConstant, outputExpr.Location, "Cannot change constant value"); + } else if (outputVar is Field { IsConst: true }) + compiler.Emit(WarningCode.WriteToConstant, outputExpr.Location, "Cannot change constant value"); ProcessStatementForList(list, outputVar, statementFor.DMTypes, statementFor.Body); break; diff --git a/DMCompiler/DM/DMCodeTree.Vars.cs b/DMCompiler/DM/DMCodeTree.Vars.cs index b259047fb3..3c399703de 100644 --- a/DMCompiler/DM/DMCodeTree.Vars.cs +++ b/DMCompiler/DM/DMCodeTree.Vars.cs @@ -174,9 +174,16 @@ private bool AlreadyExists(DMCompiler compiler, DMObject dmObject) { $"Duplicate definition of static var \"{VarName}\""); return true; } else if (dmObject.HasLocalVariable(VarName)) { - if (!varDef.Location.InDMStandard) // Duplicate instance vars are not an error in DMStandard - compiler.Emit(WarningCode.InvalidVarDefinition, varDef.Location, + if (!varDef.Location.InDMStandard) { // Duplicate instance vars are not an error in DMStandard + var variable = dmObject.GetVariable(VarName); + if(variable!.Value is not null) + compiler.Emit(WarningCode.InvalidVarDefinition, varDef.Location, + $"Duplicate definition of var \"{VarName}\". Previous definition at {variable.Value.Location}"); + else + compiler.Emit(WarningCode.InvalidVarDefinition, varDef.Location, $"Duplicate definition of var \"{VarName}\""); + } + return true; } else if (IsStatic && VarName == "vars" && dmObject == compiler.DMObjectTree.Root) { compiler.Emit(WarningCode.InvalidVarDefinition, varDef.Location, "Duplicate definition of global.vars"); diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs index 376fc8a2bd..465394322a 100644 --- a/DMCompiler/DM/DMProc.cs +++ b/DMCompiler/DM/DMProc.cs @@ -170,10 +170,9 @@ public void ValidateReturnType(DMExpression expr) { } public ProcDefinitionJson GetJsonRepresentation() { - var optimizer = new BytecodeOptimizer(); var serializer = new AnnotatedBytecodeSerializer(_compiler); - optimizer.Optimize(_compiler, AnnotatedBytecode.GetAnnotatedBytecode()); + _compiler.BytecodeOptimizer.Optimize(AnnotatedBytecode.GetAnnotatedBytecode()); List? arguments = null; if (_parameters.Count > 0) { diff --git a/DMCompiler/DM/Expressions/Builtins.cs b/DMCompiler/DM/Expressions/Builtins.cs index 4b7ec230b6..e11b037cf7 100644 --- a/DMCompiler/DM/Expressions/Builtins.cs +++ b/DMCompiler/DM/Expressions/Builtins.cs @@ -505,6 +505,13 @@ public override void EmitPushValue(ExpressionContext ctx) { return; } + if (Expression is Arglist arglist) { + // This happens silently in BYOND + ctx.Compiler.Emit(WarningCode.PointlessBuiltinCall, Location, "calling initial() on arglist() returns the current value"); + arglist.EmitPushArglist(ctx); + return; + } + ctx.Compiler.Emit(WarningCode.BadArgument, Expression.Location, $"can't get initial value of {Expression}"); ctx.Proc.Error(); } diff --git a/DMCompiler/DM/Expressions/LValue.cs b/DMCompiler/DM/Expressions/LValue.cs index a32902f4f9..d5fd05f468 100644 --- a/DMCompiler/DM/Expressions/LValue.cs +++ b/DMCompiler/DM/Expressions/LValue.cs @@ -122,6 +122,7 @@ public override void EmitPushInitial(ExpressionContext ctx) { // Identifier of field internal sealed class Field(Location location, DMVariable variable, DMComplexValueType valType) : LValue(location, variable.Type) { + public bool IsConst { get; } = variable.IsConst; public override DMComplexValueType ValType => valType; public override void EmitPushInitial(ExpressionContext ctx) { diff --git a/DMCompiler/DMCompiler.cs b/DMCompiler/DMCompiler.cs index 65cac0c3b9..d386a8e69f 100644 --- a/DMCompiler/DMCompiler.cs +++ b/DMCompiler/DMCompiler.cs @@ -14,6 +14,7 @@ using DMCompiler.Compiler.DM.AST; using DMCompiler.DM.Builders; using DMCompiler.Json; +using DMCompiler.Optimizer; namespace DMCompiler; @@ -31,11 +32,13 @@ public class DMCompiler { internal readonly DMCodeTree DMCodeTree; internal readonly DMObjectTree DMObjectTree; internal readonly DMProc GlobalInitProc; + internal readonly BytecodeOptimizer BytecodeOptimizer; public DMCompiler() { DMCodeTree = new(this); DMObjectTree = new(this); GlobalInitProc = new(this, -1, DMObjectTree.Root, null); + BytecodeOptimizer = new BytecodeOptimizer(this); } public bool Compile(DMCompilerSettings settings) { diff --git a/DMCompiler/DMStandard/DefaultPragmaConfig.dm b/DMCompiler/DMStandard/DefaultPragmaConfig.dm index f6adf4e312..103b84ab59 100644 --- a/DMCompiler/DMStandard/DefaultPragmaConfig.dm +++ b/DMCompiler/DMStandard/DefaultPragmaConfig.dm @@ -52,4 +52,5 @@ #pragma AssignmentInConditional warning #pragma PickWeightedSyntax disabled #pragma AmbiguousInOrder warning +#pragma ExtraToken warning #pragma RuntimeSearchOperator disabled diff --git a/DMCompiler/Optimizer/BytecodeOptimizer.cs b/DMCompiler/Optimizer/BytecodeOptimizer.cs index 89389db81b..dc1ffb7c7f 100644 --- a/DMCompiler/Optimizer/BytecodeOptimizer.cs +++ b/DMCompiler/Optimizer/BytecodeOptimizer.cs @@ -2,8 +2,10 @@ namespace DMCompiler.Optimizer; -public class BytecodeOptimizer { - internal void Optimize(DMCompiler compiler, List input) { +public class BytecodeOptimizer(DMCompiler compiler) { + private readonly PeepholeOptimizer _peepholeOptimizer = new(compiler); + + internal void Optimize(List input) { if (input.Count == 0) return; @@ -11,10 +13,10 @@ internal void Optimize(DMCompiler compiler, List input) { JoinAndForwardLabels(input); RemoveUnreferencedLabels(input); - PeepholeOptimizer.RunPeephole(compiler, input); + _peepholeOptimizer.RunPeephole(input); } - private static void RemoveUnreferencedLabels(List input) { + private void RemoveUnreferencedLabels(List input) { Dictionary labelReferences = new(); for (int i = 0; i < input.Count; i++) { if (input[i] is AnnotatedBytecodeLabel label) { @@ -38,7 +40,7 @@ private static void RemoveUnreferencedLabels(List input) { } } - private static void JoinAndForwardLabels(List input) { + private void JoinAndForwardLabels(List input) { Dictionary labelAliases = new(); for (int i = 0; i < input.Count; i++) { if (input[i] is AnnotatedBytecodeLabel label) { @@ -74,7 +76,7 @@ private static void JoinAndForwardLabels(List input) { } } - private static bool TryGetLabelName(AnnotatedBytecodeInstruction instruction, [NotNullWhen(true)] out string? labelName) { + private bool TryGetLabelName(AnnotatedBytecodeInstruction instruction, [NotNullWhen(true)] out string? labelName) { foreach (var arg in instruction.GetArgs()) { if (arg is not AnnotatedBytecodeLabel label) continue; diff --git a/DMCompiler/Optimizer/PeepholeOptimizations.cs b/DMCompiler/Optimizer/PeepholeOptimizations.cs index 56f6f4577f..6b2b9ed3e1 100644 --- a/DMCompiler/Optimizer/PeepholeOptimizations.cs +++ b/DMCompiler/Optimizer/PeepholeOptimizations.cs @@ -116,6 +116,34 @@ public void Apply(DMCompiler compiler, List input, int index } } +// PushReferenceValue [ref] +// PushString [string] +// DereferenceIndex +// -> IndexRefWithString [ref, string] +internal sealed class IndexRefWithString : IOptimization { + public OptPass OptimizationPass => OptPass.PeepholeOptimization; + + public ReadOnlySpan GetOpcodes() { + return [ + DreamProcOpcode.PushReferenceValue, + DreamProcOpcode.PushString, + DreamProcOpcode.DereferenceIndex + ]; + } + + public void Apply(DMCompiler compiler, List input, int index) { + AnnotatedBytecodeInstruction firstInstruction = (AnnotatedBytecodeInstruction)(input[index]); + AnnotatedBytecodeReference pushVal = firstInstruction.GetArg(0); + + AnnotatedBytecodeInstruction secondInstruction = (AnnotatedBytecodeInstruction)(input[index + 1]); + AnnotatedBytecodeString strIndex = secondInstruction.GetArg(0); + + input.RemoveRange(index, 3); + input.Insert(index, new AnnotatedBytecodeInstruction(DreamProcOpcode.IndexRefWithString, -1, + [pushVal, strIndex])); + } +} + // PushReferenceValue [ref] // Return // -> ReturnReferenceValue [ref] @@ -186,6 +214,24 @@ public void Apply(DMCompiler compiler, List input, int index } } +// Return +// Jump [label] +// -> Return +internal sealed class RemoveJumpAfterReturn : IOptimization { + public OptPass OptimizationPass => OptPass.PeepholeOptimization; + + public ReadOnlySpan GetOpcodes() { + return [ + DreamProcOpcode.Return, + DreamProcOpcode.Jump + ]; + } + + public void Apply(DMCompiler compiler, List input, int index) { + input.RemoveRange(index + 1, 1); + } +} + // PushFloat [float] // SwitchCase [label] // -> SwitchOnFloat [float] [label] diff --git a/DMCompiler/Optimizer/PeepholeOptimizer.cs b/DMCompiler/Optimizer/PeepholeOptimizer.cs index aad7cd7287..73eb36c776 100644 --- a/DMCompiler/Optimizer/PeepholeOptimizer.cs +++ b/DMCompiler/Optimizer/PeepholeOptimizer.cs @@ -40,6 +40,8 @@ internal enum OptPass : byte { // ReSharper disable once ClassNeverInstantiated.Global internal sealed class PeepholeOptimizer { + private readonly DMCompiler _compiler; + private class OptimizationTreeEntry { public IOptimization? Optimization; public Dictionary? Children; @@ -48,47 +50,44 @@ private class OptimizationTreeEntry { /// /// The optimization passes in the order that they run /// - private static readonly OptPass[] Passes; + private readonly OptPass[] _passes; /// /// Trees matching chains of opcodes to peephole optimizations /// - private static readonly Dictionary[] OptimizationTrees; - - static PeepholeOptimizer() { - Passes = (OptPass[])Enum.GetValues(typeof(OptPass)); - OptimizationTrees = new Dictionary[Passes.Length]; - for (int i = 0; i < OptimizationTrees.Length; i++) { - OptimizationTrees[i] = new Dictionary(); + private readonly Dictionary[] _optimizationTrees; + + public PeepholeOptimizer(DMCompiler compiler) { + _compiler = compiler; + _passes = (OptPass[])Enum.GetValues(typeof(OptPass)); + _optimizationTrees = new Dictionary[_passes.Length]; + for (int i = 0; i < _optimizationTrees.Length; i++) { + _optimizationTrees[i] = new Dictionary(); } } - /// Setup for each - private static void GetOptimizations(DMCompiler compiler) { - var possibleTypes = typeof(IOptimization).Assembly.GetTypes(); - var optimizationTypes = new List(possibleTypes.Length); - - foreach (var type in possibleTypes) { - if (typeof(IOptimization).IsAssignableFrom(type) && type is { IsClass: true, IsAbstract: false }) { - optimizationTypes.Add(type); - } - } + /// Setup for each + private void GetOptimizations() { + foreach (var optType in typeof(IOptimization).Assembly.GetTypes()) { + if (!typeof(IOptimization).IsAssignableFrom(optType) || + optType is not { IsClass: true, IsAbstract: false }) + continue; - foreach (var optType in optimizationTypes) { var opt = (IOptimization)(Activator.CreateInstance(optType)!); var opcodes = opt.GetOpcodes(); if (opcodes.Length < 2) { - compiler.ForcedError(Location.Internal, $"Peephole optimization {optType} must have at least 2 opcodes"); + _compiler.ForcedError(Location.Internal, + $"Peephole optimization {optType} must have at least 2 opcodes"); continue; } - if (!OptimizationTrees[(byte)opt.OptimizationPass].TryGetValue(opcodes[0], out var treeEntry)) { + if (!_optimizationTrees[(byte)opt.OptimizationPass].TryGetValue(opcodes[0], out var treeEntry)) { treeEntry = new() { Children = new() }; - OptimizationTrees[(byte)opt.OptimizationPass].Add(opcodes[0], treeEntry); + _optimizationTrees[(byte)opt.OptimizationPass].Add(opcodes[0], treeEntry); } for (int i = 1; i < opcodes.Length; i++) { @@ -107,14 +106,14 @@ private static void GetOptimizations(DMCompiler compiler) { } } - public static void RunPeephole(DMCompiler compiler, List input) { - GetOptimizations(compiler); - foreach (var optPass in Passes) { - RunPass(compiler, (byte)optPass, input); + public void RunPeephole(List input) { + GetOptimizations(); + foreach (var optPass in _passes) { + RunPass((byte)optPass, input); } } - private static void RunPass(DMCompiler compiler, byte pass, List input) { + private void RunPass(byte pass, List input) { OptimizationTreeEntry? currentOpt = null; int optSize = 0; @@ -125,7 +124,7 @@ int AttemptCurrentOpt(int i) { int offset; if (currentOpt.Optimization?.CheckPreconditions(input, i - optSize) is true) { - currentOpt.Optimization.Apply(compiler, input, i - optSize); + currentOpt.Optimization.Apply(_compiler, input, i - optSize); offset = (optSize + 2); // Run over the new opcodes for potential further optimization } else { // This chain of opcodes did not lead to a valid optimization. @@ -141,7 +140,7 @@ int AttemptCurrentOpt(int i) { var bytecode = input[i]; if (bytecode is not AnnotatedBytecodeInstruction instruction) { i -= AttemptCurrentOpt(i); - i = Math.Max(i, 0); + i = Math.Max(i, -1); // i++ brings -1 back to 0 continue; } @@ -149,7 +148,7 @@ int AttemptCurrentOpt(int i) { if (currentOpt == null) { optSize = 1; - OptimizationTrees[pass].TryGetValue(opcode, out currentOpt); + _optimizationTrees[pass].TryGetValue(opcode, out currentOpt); continue; } @@ -160,7 +159,7 @@ int AttemptCurrentOpt(int i) { } i -= AttemptCurrentOpt(i); - i = Math.Max(i, 0); + i = Math.Max(i, -1); // i++ brings -1 back to 0 } AttemptCurrentOpt(input.Count); diff --git a/DMDisassembler/Program.cs b/DMDisassembler/Program.cs index 638358c600..0a4f5dd409 100644 --- a/DMDisassembler/Program.cs +++ b/DMDisassembler/Program.cs @@ -1,9 +1,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using System.Text.Json; using DMCompiler.Json; +using JetBrains.Annotations; namespace DMDisassembler; @@ -13,6 +15,7 @@ internal class Program { public static DMProc GlobalInitProc = null; public static List Procs = null; public static Dictionary AllTypes = null; + public static List TypesById = null; private static readonly string NoTypeSelectedMessage = "No type is selected"; @@ -52,6 +55,8 @@ static void Main(string[] args) { } } + Console.WriteLine("DM Disassembler for OpenDream. Enter a command or \"help\" for more information."); + bool acceptingCommands = true; while (acceptingCommands) { if (_selectedType != null) { @@ -71,6 +76,7 @@ static void Main(string[] args) { switch (command) { case "quit": + case "exit": case "q": acceptingCommands = false; break; case "search": Search(split); break; case "sel": @@ -78,25 +84,105 @@ static void Main(string[] args) { case "list": List(split); break; case "d": case "decompile": Decompile(split); break; + case "stats": Stats(GetArg()); break; case "test-all": TestAll(); break; case "dump-all": DumpAll(); break; - case "help": PrintHelp(); break; - default: Console.WriteLine("Invalid command \"" + command + "\""); break; + case "help": { + PrintHelp(GetArg()); + break; + } + default: Console.WriteLine($"Invalid command \"{command}\""); break; + } + + [CanBeNull] + string GetArg() { + if (split.Length > 2) { + Console.WriteLine($"Command \"{command}\" takes 0 or 1 arguments. Ignoring extra arguments."); + } + + return split.Length > 1 ? split[1] : null; } } } - private static void PrintHelp() { - Console.WriteLine("DM Disassembler for OpenDream"); - Console.WriteLine("Commands and arguments:"); - Console.WriteLine("help : Show this help"); - Console.WriteLine("quit|q : Exits the disassembler"); - Console.WriteLine("search type|proc [name] : Search for a particular typepath or a proc on a selected type"); - Console.WriteLine("select|sel : Select a typepath to run further commands on"); - Console.WriteLine("list procs|globals : List all globals, or all procs on a selected type"); - Console.WriteLine("decompile|d [name] : Decompiles the proc on the selected type"); - Console.WriteLine("dump-all : Decompiles every proc and writes the output to a file"); - Console.WriteLine("test-all : Tries to decompile every single proc to check for issues with this disassembler; not for production use"); + private static void PrintHelp([CanBeNull] string command) { + if (string.IsNullOrEmpty(command)) { + AllCommands(); + return; + } + + command = command.ToLower(); + + switch (command) { + case "stats": { + Console.WriteLine("Prints various statistics. Usage: stats [type]"); + Console.WriteLine("Options for [type]:"); + Console.WriteLine("procs-by-type : Prints the number of proc declarations (not overrides) on each type in descending order"); + break; + } + default: { + Console.WriteLine($"No additional help for \"{command}\""); + AllCommands(); + break; + } + } + + void AllCommands() { + Console.WriteLine("DM Disassembler for OpenDream"); + Console.WriteLine("Commands and arguments:"); + Console.WriteLine("help [command] : Show additional help for [command] if applicable"); + Console.WriteLine("exit|quit|q : Exits the disassembler"); + Console.WriteLine("search type|proc [name] : Search for a particular typepath or a proc on a selected type"); + Console.WriteLine("select|sel : Select a typepath to run further commands on"); + Console.WriteLine("list procs|globals : List all globals, or all procs on a selected type"); + Console.WriteLine("decompile|d [name] : Decompiles the proc on the selected type"); + Console.WriteLine("stats [type] : Prints various stats about the game. Use \"help stats\" for more info"); + Console.WriteLine("dump-all : Decompiles every proc and writes the output to a file"); + Console.WriteLine("test-all : Tries to decompile every single proc to check for issues with this disassembler; not for production use"); + } + } + + private static void Stats([CanBeNull] string statType) { + if (string.IsNullOrEmpty(statType)) { + PrintHelp("stats"); + return; + } + + switch (statType) { + case "procs-by-type": { + ProcsByType(); + return; + } + default: { + Console.WriteLine($"Unknown stat \"{statType}\""); + PrintHelp("stats"); + return; + } + } + + void ProcsByType() { + Console.WriteLine("Counting all proc declarations (no overrides) by type. This may take a moment."); + Dictionary typeIdToProcCount = new Dictionary(); + foreach (DMProc proc in Procs) { + if(proc.IsOverride || proc.Name == "") continue; // Don't count overrides or procs + if (typeIdToProcCount.TryGetValue(proc.OwningTypeId, out var count)) { + typeIdToProcCount[proc.OwningTypeId] = count + 1; + } else { + typeIdToProcCount[proc.OwningTypeId] = 1; + } + } + + Console.WriteLine("Type: Proc Declarations"); + foreach (var pair in typeIdToProcCount.OrderByDescending(kvp => kvp.Value)) { + + var type = TypesById[pair.Key]; + if (pair.Key == 0) { + Console.WriteLine($": {pair.Value}"); + } else { + Console.WriteLine($"{type.Path}: {pair.Value}"); + } + } + } } private static void Search(string[] args) { @@ -224,9 +310,12 @@ private static void LoadAllProcs() { private static void LoadAllTypes() { AllTypes = new Dictionary(CompiledJson.Types.Length); + TypesById = new List(CompiledJson.Types.Length); foreach (DreamTypeJson json in CompiledJson.Types) { - AllTypes.Add(json.Path, new DMType(json)); + var dmType = new DMType(json); + AllTypes.Add(json.Path, dmType); + TypesById.Add(dmType); } //Add global procs to the root type diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 29f372e2e1..51a05c774d 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -2556,6 +2556,17 @@ public static ProcStatus DereferenceIndex(DMProcState state) { return ProcStatus.Continue; } + public static ProcStatus IndexRefWithString(DMProcState state) { + DreamReference reference = state.ReadReference(); + var refValue = state.GetReferenceValue(reference); + + var index = new DreamValue(state.ReadString()); + var indexResult = state.GetIndex(refValue, index, state); + + state.Push(indexResult); + return ProcStatus.Continue; + } + public static ProcStatus DereferenceCall(DMProcState state) { string name = state.ReadString(); var argumentInfo = state.ReadProcArguments(); diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index e55d67ad6a..b5ad9fcc8f 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -283,6 +283,7 @@ public sealed class DMProcState : ProcState { {DreamProcOpcode.JumpIfFalseReference, DMOpcodeHandlers.JumpIfFalseReference}, {DreamProcOpcode.DereferenceField, DMOpcodeHandlers.DereferenceField}, {DreamProcOpcode.DereferenceIndex, DMOpcodeHandlers.DereferenceIndex}, + {DreamProcOpcode.IndexRefWithString, DMOpcodeHandlers.IndexRefWithString}, {DreamProcOpcode.DereferenceCall, DMOpcodeHandlers.DereferenceCall}, {DreamProcOpcode.PopReference, DMOpcodeHandlers.PopReference}, {DreamProcOpcode.BitShiftLeftReference,DMOpcodeHandlers.BitShiftLeftReference}, diff --git a/OpenDreamRuntime/Procs/ProcDecoder.cs b/OpenDreamRuntime/Procs/ProcDecoder.cs index 2b0ac676f6..2fd2d2f41f 100644 --- a/OpenDreamRuntime/Procs/ProcDecoder.cs +++ b/OpenDreamRuntime/Procs/ProcDecoder.cs @@ -119,6 +119,7 @@ public ITuple DecodeInstruction() { return (opcode, ReadReference(), ReadReference()); case DreamProcOpcode.PushRefAndDereferenceField: + case DreamProcOpcode.IndexRefWithString: return (opcode, ReadReference(), ReadString()); case DreamProcOpcode.CallStatement: