From b1134c3e4e7023b2432ff50f3b1db9d82fe7d471 Mon Sep 17 00:00:00 2001 From: ike709 Date: Tue, 18 Feb 2025 00:10:00 -0600 Subject: [PATCH] Implement `astype()` (#2211) Co-authored-by: ike709 Co-authored-by: wixoa --- .../DMProject/Tests/Builtins/astype.dm | 18 ++++++++ DMCompiler/Bytecode/DreamProcOpcode.cs | 3 +- .../Compiler/DM/AST/DMAST.ExpressionBinary.cs | 1 + .../Compiler/DM/AST/DMAST.ExpressionUnary.cs | 1 + DMCompiler/Compiler/DM/DMParser.cs | 10 +++++ DMCompiler/DM/Builders/DMExpressionBuilder.cs | 35 +++++++++------ DMCompiler/DM/DMProc.cs | 4 ++ DMCompiler/DM/Expressions/Builtins.cs | 24 ++++++++++ OpenDreamRuntime/Procs/DMOpcodeHandlers.cs | 44 +++++++++++++------ OpenDreamRuntime/Procs/DMProc.cs | 1 + 10 files changed, 113 insertions(+), 28 deletions(-) create mode 100644 Content.Tests/DMProject/Tests/Builtins/astype.dm diff --git a/Content.Tests/DMProject/Tests/Builtins/astype.dm b/Content.Tests/DMProject/Tests/Builtins/astype.dm new file mode 100644 index 0000000000..0a41b553ff --- /dev/null +++ b/Content.Tests/DMProject/Tests/Builtins/astype.dm @@ -0,0 +1,18 @@ + +/datum/foo +/datum/foo/bar + +/proc/test_null() + var/datum/D = new + var/datum/foo/bar/B = astype(D) + return isnull(B) + +/proc/test_type() + var/datum/foo/bar/B = new + var/datum/D = astype(B) + var/datum/foo/F = astype(D) + return F.type + +/proc/RunTest() + ASSERT(test_null()) + ASSERT(test_type() == /datum/foo/bar) diff --git a/DMCompiler/Bytecode/DreamProcOpcode.cs b/DMCompiler/Bytecode/DreamProcOpcode.cs index d583165388..00852ad0bb 100644 --- a/DMCompiler/Bytecode/DreamProcOpcode.cs +++ b/DMCompiler/Bytecode/DreamProcOpcode.cs @@ -140,7 +140,8 @@ public enum DreamProcOpcode : byte { Ftp = 0x46, [OpcodeMetadata(-1)] Initial = 0x47, - //0x48 + [OpcodeMetadata(-1)] + AsType = 0x48, [OpcodeMetadata(-1)] IsType = 0x49, [OpcodeMetadata(-2)] diff --git a/DMCompiler/Compiler/DM/AST/DMAST.ExpressionBinary.cs b/DMCompiler/Compiler/DM/AST/DMAST.ExpressionBinary.cs index aa024889f6..00d97cbbd1 100644 --- a/DMCompiler/Compiler/DM/AST/DMAST.ExpressionBinary.cs +++ b/DMCompiler/Compiler/DM/AST/DMAST.ExpressionBinary.cs @@ -48,6 +48,7 @@ public sealed class DMASTPower(Location location, DMASTExpression a, DMASTExpres public sealed class DMASTAdd(Location location, DMASTExpression a, DMASTExpression b) : DMASTBinary(location, a, b); public sealed class DMASTSubtract(Location location, DMASTExpression a, DMASTExpression b) : DMASTBinary(location, a, b); public sealed class DMASTArctan2(Location location, DMASTExpression xExpression, DMASTExpression yExpression) : DMASTBinary(location, xExpression, yExpression); +public sealed class DMASTAsType(Location location, DMASTExpression value, DMASTExpression type) : DMASTBinary(location, value, type); public sealed class DMASTIsType(Location location, DMASTExpression value, DMASTExpression type) : DMASTBinary(location, value, type); public sealed class DMASTGetStep(Location location, DMASTExpression refValue, DMASTExpression dir) : DMASTBinary(location, refValue, dir); public sealed class DMASTGetDir(Location location, DMASTExpression loc1, DMASTExpression loc2) : DMASTBinary(location, loc1, loc2); diff --git a/DMCompiler/Compiler/DM/AST/DMAST.ExpressionUnary.cs b/DMCompiler/Compiler/DM/AST/DMAST.ExpressionUnary.cs index eee1db30da..aee10c3427 100644 --- a/DMCompiler/Compiler/DM/AST/DMAST.ExpressionUnary.cs +++ b/DMCompiler/Compiler/DM/AST/DMAST.ExpressionUnary.cs @@ -31,6 +31,7 @@ public sealed class DMASTNameof(Location location, DMASTExpression expression) : public sealed class DMASTIsSaved(Location location, DMASTExpression expression) : DMASTUnary(location, expression); public sealed class DMASTIsNull(Location location, DMASTExpression value) : DMASTUnary(location, value); public sealed class DMASTLength(Location location, DMASTExpression value) : DMASTUnary(location, value); +public sealed class DMASTImplicitAsType(Location location, DMASTExpression value) : DMASTUnary(location, value); public sealed class DMASTImplicitIsType(Location location, DMASTExpression value) : DMASTUnary(location, value); /// diff --git a/DMCompiler/Compiler/DM/DMParser.cs b/DMCompiler/Compiler/DM/DMParser.cs index c0ecacbba4..ec01b396ae 100644 --- a/DMCompiler/Compiler/DM/DMParser.cs +++ b/DMCompiler/Compiler/DM/DMParser.cs @@ -2641,6 +2641,16 @@ private void BracketWhitespace() { ? new DMASTLog(callLoc, callParameters[0].Value, null) : new DMASTLog(callLoc, callParameters[1].Value, callParameters[0].Value); } + case "astype": { + if (callParameters.Length != 1 && callParameters.Length != 2) { + Emit(WarningCode.InvalidArgumentCount, callLoc, "astype() requires 1 or 2 arguments"); + return new DMASTInvalidExpression(callLoc); + } + + return callParameters.Length == 1 + ? new DMASTImplicitAsType(callLoc, callParameters[0].Value) + : new DMASTAsType(callLoc, callParameters[0].Value, callParameters[1].Value); + } case "istype": { if (callParameters.Length != 1 && callParameters.Length != 2) { Emit(WarningCode.InvalidArgumentCount, callLoc, "istype() requires 1 or 2 arguments"); diff --git a/DMCompiler/DM/Builders/DMExpressionBuilder.cs b/DMCompiler/DM/Builders/DMExpressionBuilder.cs index 9e25dc50fe..9ef101ba4a 100644 --- a/DMCompiler/DM/Builders/DMExpressionBuilder.cs +++ b/DMCompiler/DM/Builders/DMExpressionBuilder.cs @@ -74,6 +74,7 @@ private DMExpression BuildExpression(DMASTExpression expression, DreamPath? infe case DMASTNotEqual notEqual: result = BuildNotEqual(notEqual, inferredPath); break; case DMASTDereference deref: result = BuildDereference(deref, inferredPath); break; case DMASTLocate locate: result = BuildLocate(locate, inferredPath); break; + case DMASTImplicitAsType implicitAsType: result = BuildImplicitAsType(implicitAsType, inferredPath); break; case DMASTImplicitIsType implicitIsType: result = BuildImplicitIsType(implicitIsType, inferredPath); break; case DMASTList list: result = BuildList(list, inferredPath); break; case DMASTDimensionalList dimensionalList: result = BuildDimensionalList(dimensionalList, inferredPath); break; @@ -338,25 +339,21 @@ private DMExpression BuildExpression(DMASTExpression expression, DreamPath? infe BuildExpression(locateCoordinates.Y, inferredPath), BuildExpression(locateCoordinates.Z, inferredPath)); break; + case DMASTAsType asType: { + var lhs = BuildExpression(asType.LHS, inferredPath); + var rhs = BuildExpression(asType.RHS, lhs.Path); + + result = new AsType(asType.Location, lhs, rhs); + break; + } case DMASTIsSaved isSaved: result = new IsSaved(isSaved.Location, BuildExpression(isSaved.Value, inferredPath)); break; case DMASTIsType isType: { - if (isType.RHS is DMASTIdentifier { Identifier: "__IMPLIED_TYPE__" }) { - var expr = BuildExpression(isType.LHS, inferredPath); - if (expr.Path is null) { - result = BadExpression(WarningCode.BadExpression, isType.Location, - "A type could not be inferred!"); - break; - } + var lhs = BuildExpression(isType.LHS, inferredPath); + var rhs = BuildExpression(isType.RHS, lhs.Path); - result = new IsTypeInferred(isType.Location, expr, expr.Path.Value); - break; - } - - result = new IsType(isType.Location, - BuildExpression(isType.LHS, inferredPath), - BuildExpression(isType.RHS, inferredPath)); + result = new IsType(isType.Location, lhs, rhs); break; } @@ -1072,6 +1069,16 @@ private DMExpression BuildLocate(DMASTLocate locate, DreamPath? inferredPath) { return new Locate(locate.Location, pathExpr, container); } + private DMExpression BuildImplicitAsType(DMASTImplicitAsType asType, DreamPath? inferredPath) { + var expr = BuildExpression(asType.Value, inferredPath); + + if (inferredPath is null) { + return BadExpression(WarningCode.BadExpression, asType.Location, "Could not infer a type"); + } + + return new AsTypeInferred(asType.Location, expr, inferredPath.Value); + } + private DMExpression BuildImplicitIsType(DMASTImplicitIsType isType, DreamPath? inferredPath) { var expr = BuildExpression(isType.Value, inferredPath); diff --git a/DMCompiler/DM/DMProc.cs b/DMCompiler/DM/DMProc.cs index 4dfa008168..670442f16b 100644 --- a/DMCompiler/DM/DMProc.cs +++ b/DMCompiler/DM/DMProc.cs @@ -969,6 +969,10 @@ public void IsSaved() { WriteOpcode(DreamProcOpcode.IsSaved); } + public void AsType() { + WriteOpcode(DreamProcOpcode.AsType); + } + public void IsType() { WriteOpcode(DreamProcOpcode.IsType); } diff --git a/DMCompiler/DM/Expressions/Builtins.cs b/DMCompiler/DM/Expressions/Builtins.cs index e11b037cf7..6092ce591d 100644 --- a/DMCompiler/DM/Expressions/Builtins.cs +++ b/DMCompiler/DM/Expressions/Builtins.cs @@ -291,6 +291,30 @@ public override void EmitPushValue(ExpressionContext ctx) { } } +// astype(x, y) +internal sealed class AsType(Location location, DMExpression expr, DMExpression path) : DMExpression(location) { + public override void EmitPushValue(ExpressionContext ctx) { + expr.EmitPushValue(ctx); + path.EmitPushValue(ctx); + ctx.Proc.AsType(); + } +} + +// astype(x) +internal sealed class AsTypeInferred(Location location, DMExpression expr, DreamPath path) : DMExpression(location) { + public override void EmitPushValue(ExpressionContext ctx) { + if (!ctx.ObjectTree.TryGetTypeId(path, out var typeId)) { + ctx.Compiler.Emit(WarningCode.ItemDoesntExist, Location, $"Type {path} does not exist"); + + return; + } + + expr.EmitPushValue(ctx); + ctx.Proc.PushType(typeId); + ctx.Proc.AsType(); + } +} + // istype(x, y) internal sealed class IsType(Location location, DMExpression expr, DMExpression path) : DMExpression(location) { public override DMComplexValueType ValType => DMValueType.Num; diff --git a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs index 5c6fbff735..777574d9dc 100644 --- a/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs +++ b/OpenDreamRuntime/Procs/DMOpcodeHandlers.cs @@ -1403,39 +1403,57 @@ public static ProcStatus IsInRange(DMProcState state) { return ProcStatus.Continue; } + public static ProcStatus AsType(DMProcState state) { + DreamValue typeValue = state.Pop(); + DreamValue value = state.Pop(); + + state.Push(TypecheckHelper(typeValue, value, true)); + + return ProcStatus.Continue; + } + public static ProcStatus IsType(DMProcState state) { DreamValue typeValue = state.Pop(); DreamValue value = state.Pop(); + + state.Push(TypecheckHelper(typeValue, value, false)); + + return ProcStatus.Continue; + } + + private static DreamValue TypecheckHelper(DreamValue typeValue, DreamValue value, bool doCast) { + // astype() returns null, istype() returns false + DreamValue nullOrFalse = doCast ? DreamValue.Null : DreamValue.False; TreeEntry? type; if (typeValue.TryGetValueAsDreamObject(out var typeObject)) { if (typeObject == null) { - state.Push(DreamValue.False); - return ProcStatus.Continue; + return nullOrFalse; } type = typeObject.ObjectDefinition.TreeEntry; } else if (typeValue.TryGetValueAsAppearance(out _)) { // /image matches an appearance - state.Push(value.TryGetValueAsDreamObject(out _) - ? DreamValue.True - : DreamValue.False); + if (value.TryGetValueAsDreamObject(out var imageObject)) { + return doCast ? new DreamValue(imageObject) : DreamValue.True; + } - return ProcStatus.Continue; + return nullOrFalse; } else if (!typeValue.TryGetValueAsType(out type)) { - state.Push(DreamValue.False); - - return ProcStatus.Continue; + return nullOrFalse; } if (value.TryGetValueAsDreamObject(out var dreamObject) && dreamObject != null) { - state.Push(new DreamValue(dreamObject.IsSubtypeOf(type) ? 1 : 0)); - } else { - state.Push(DreamValue.False); + if (dreamObject.IsSubtypeOf(type)) { + return doCast ? new DreamValue(dreamObject) : DreamValue.True; + } + + return nullOrFalse; } - return ProcStatus.Continue; + return nullOrFalse; } + #endregion Comparisons #region Flow diff --git a/OpenDreamRuntime/Procs/DMProc.cs b/OpenDreamRuntime/Procs/DMProc.cs index 9b8a536854..f0913cd35b 100644 --- a/OpenDreamRuntime/Procs/DMProc.cs +++ b/OpenDreamRuntime/Procs/DMProc.cs @@ -251,6 +251,7 @@ public sealed class DMProcState : ProcState { {DreamProcOpcode.Link, DMOpcodeHandlers.Link}, {DreamProcOpcode.Ftp, DMOpcodeHandlers.Ftp}, {DreamProcOpcode.Initial, DMOpcodeHandlers.Initial}, + {DreamProcOpcode.AsType, DMOpcodeHandlers.AsType}, {DreamProcOpcode.IsType, DMOpcodeHandlers.IsType}, {DreamProcOpcode.LocateCoord, DMOpcodeHandlers.LocateCoord}, {DreamProcOpcode.Locate, DMOpcodeHandlers.Locate},