"Why?" (a good question) and "Don't do that!" aside, here's a technical answer: override %OnDetermineClass to allow instances of the parent class to be treated as the subclass in question. For example:

Class DC.Demo.Extension.BaseRecord Extends %Persistent
{

Property Name As %String;

ClassMethod %OnDetermineClass(oid As %ObjectIdentity, ByRef class As %String) As %Status [ ServerOnly = 1 ]
{
  Set tSC = ##super(oid,.class)
  If (class = "DC.Demo.Extension.BaseRecord") {
    Set class = $classname()
  }
  Quit tSC
}

Storage Default
{
<Data name="BaseRecordDefaultData">
<Value name="1">
<Value>%%CLASSNAME</Value>
</Value>
<Value name="2">
<Value>Name</Value>
</Value>
</Data>
<DataLocation>^DC.Demo.Extension.BaseRecordD</DataLocation>
<DefaultData>BaseRecordDefaultData</DefaultData>
<IdLocation>^DC.Demo.Extension.BaseRecordD</IdLocation>
<IndexLocation>^DC.Demo.Extension.BaseRecordI</IndexLocation>
<StreamLocation>^DC.Demo.Extension.BaseRecordS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}

}

Class DC.Demo.Extension.SubRecord Extends BaseRecord
{

Property Foo As %String;

Storage Default
{
<Data name="SubRecordDefaultData">
<Subscript>"SubRecord"</Subscript>
<Value name="1">
<Value>Foo</Value>
</Value>
</Data>
<DefaultData>SubRecordDefaultData</DefaultData>
<Type>%Library.CacheStorage</Type>
}

}

Class DC.Demo.Extension.Driver
{

ClassMethod Run()
{
  Do ##class(DC.Demo.Extension.BaseRecord).%KillExtent()

  Set tBaseRecord = ##class(DC.Demo.Extension.BaseRecord).%New()
  Set tBaseRecord.Name = "Fred"
  Write !,"Save base record: ",tBaseRecord.%Save()

  Write !,"Contents of global ^DC.Demo.Extension.BaseRecordD:",!
  zw ^DC.Demo.Extension.BaseRecordD

  Set tSubRecord = ##class(DC.Demo.Extension.SubRecord).%OpenId(1)
  Set tSubRecord.Foo = "Bar"
  Write !,"Open as sub record, name = ",tSubRecord.Name
  Write !,"Save sub record (converts to sub record): ",tSubRecord.%Save()

  Write !,"Contents of global ^DC.Demo.Extension.BaseRecordD:",!
  zw ^DC.Demo.Extension.BaseRecordD
}

}

Output is:

USER>d ##class(DC.Demo.Extension.Driver).Run()
 
Save base record: 1
Contents of global ^DC.Demo.Extension.BaseRecordD:
^DC.Demo.Extension.BaseRecordD=1
^DC.Demo.Extension.BaseRecordD(1)=$lb("","Fred")
 
Open as sub record, name = Fred
Save sub record (converts to sub record): 1
Contents of global ^DC.Demo.Extension.BaseRecordD:
^DC.Demo.Extension.BaseRecordD=1
^DC.Demo.Extension.BaseRecordD(1)=$lb("~DC.Demo.Extension.SubRecord~","Fred")
^DC.Demo.Extension.BaseRecordD(1,"SubRecord")=$lb("Bar")

My top place by day was 242 on day 8 (that was the only day that I actually did the puzzle at midnight though - I'm not at all a night owl).

My three wishes for ObjectScript would be:

  1. A greater variety of high-performance data structures with low-level support (rather than shoehorning everything into a local array/PPG/$ListBuild list to keep it wicked fast, or building more expensive objects that do exactly what I want)
  2. Libraries for working with those (and existing) data structures (or, better yet, an OO approach to everything, similar to %Library.DynamicArray/%Library.DynamicObject)
  3. Functional programming capabilities

This isn't the answer to your actual question, but it's worth pointing out ##class(%Library.File).ManagerDirectory()  - instead of referencing the global, you could use:

##class(%Library.File).ManagerDirectory()_"LDAPKeyStore/"

Also, ##class(%SYS.System).GetInstanceName() returns the instance name; you shouldn't need to set that in a global, and (furthermore) the instance name isn't necessarily part of the path to the mgr directory (the two can be configured independently).

Other than that I think you'd be stuck with using XECUTE or $Xecute to do what you originally suggested.

After further review, I'm really not sure why the first/second queries don't use the index. The problem with the last query is that %Key is the index in the list, not anything about Tag itself.

Here's a solution that performs well in my testing:

Class DC.Demo.Tag Extends (%Persistent, %Populate)
{

Index Tag On Tag [ Unique ];

Property Tag As %String;

}

Class DC.Demo.Tagged Extends (%Persistent, %Populate)
{

Relationship HasTags As DC.Demo.HasTag [ Cardinality = children, Inverse = Tagged ];

ClassMethod Run()
{
    Do ..%KillExtent()
    Do ##class(DC.Demo.Tag).%KillExtent()
    
    Do ##class(DC.Demo.Tag).Populate(50)
    Do ..Populate(5000)
    Do ##class(DC.Demo.HasTag).Populate(10000)
}

}

Class DC.Demo.HasTag Extends (%Persistent, %Populate)
{

Relationship Tagged As DC.Demo.Tagged [ Cardinality = parent, Inverse = HasTags ];

Property Tag As DC.Demo.Tag [ Required ];

Index UniqueTag On Tag [ IdKey ];

Index TaggedByTag On (Tag, Tagged);

}

It's not the prettiest, but I think the simplest solution would be to avoid navigating to the parent object entirely:

  • Add a Foobar property to EmbedObj.
  • Via a row/object trigger in ContainerObj, propagate changes to Foobar to EmbedObj_Foobar.
    • As an initial step for data population in existing records, run SQL: update ContainerObj set EmbedObj_Foobar = Foobar
  • Base your SQLComputeCode on the copy of Foobar in the serial class.

Re: extending method keywords, you can't do that at this time, but a useful approximation is structuring a comment - for example:

/// @MyKeyword MyValue
ClassMethod TestOne()
{
    // Implementation
}

And then looking at the %Dictionary.MethodDefinition / %Dictionary.CompiledMethod documentation in a generator method. (But it looks like you might already be on to that with @AutoGenerated.)

Re: making first compilation work, this works for me, by making the first compilation automatically trigger a second one when needed:

ClassMethod GenerateMethods() As %Status [ CodeMode = objectgenerator ]
{
    For i=1:1:%class.Methods.Count() {
        #dim method As %Dictionary.MethodDefinition = %class.Methods.GetAt(i)
        Continue:((method.Description["@AutoGenerated") || (method.CodeMode="objectgenerator"))
        Do %class.Methods.Insert(##class(util.TestGenerator).Generate(method.Name_"DoSomethingElse",%class.Name))
    }
    If (%class.Methods.%IsModified()) {
        Do ##class(%Projection.AbstractProjection).QueueClass(%class.Name)
    }
    Quit %class.%Save()
}

Test code:

Class util.Driver
{

ClassMethod Run()
{
    do ##class(%Dictionary.MethodDefinition).IDKEYDelete("util.Test","TestOneDoSomethingElse")
    do ##class(%Dictionary.MethodDefinition).IDKEYDelete("util.Test","TestTwoDoSomethingElse")
    do $system.OBJ.UnCompile("util.*")
    do $system.OBJ.Compile("util.*","ck")
}

}

If you want to count (or otherwise traverse) all the elements in a multidimensional array, you can use $Query - here's a sample with method that does that:

ClassMethod Run()
{
    Set a(1) = "blue"
    Set a(1,3,5,2) = "navy"
    Set a(2) = "red"
    Set a(2,2) = "crimson"
    Set a(3) = "yellow"
    
    Write ..Count(.a)
}

ClassMethod Count(ByRef pArray) As %Integer [ PublicList = pArray ]
{
    Set count = 0
    Set ref = $Query(pArray(""))
    While (ref '= "") {
        Set count = count + 1
        Set ref = $Query(@ref)
    }
    Quit count
}

The key point here is how to get the value of the "DE" property parameter from the OPTREGISTER class definition into the "DE" property of the corresponding property. The naming does not do that automatically. Generally, property parameters (like DE, DICLookUp, XMLNAME) are used for code generation, either in methods (the third example below), projections (like XML), or for validation of datatypes (e.g., %String MINLEN/MAXLEN).

Assuming XML export is done with (for example):

Set = ##class(Test.SendExternalModel.OPTREGISTER).%New()
Do o.XMLExport(,",literal,indent")

There are at least three options. I'd recommend the third, but the first two are perhaps easier to understand.

1) In a method of OPTREGISTER (perhaps %OnNew), instantiate the various properties and set the DE property of each of them. In this case, the DE property parameter is no longer needed. For example:


Method %OnNew() As %Status [ Private, ServerOnly = 1 ]
{
  Set ..THBZ = ##class(Test.SendExternalModel.DE2).%New()
  Set ..THBZ.DE = "DEX71.41.007.01"
  Set ..GHFS = ##class(Test.SendExternalModel.DE2).%New()
  Set ..GHFS.DE = "DEX71.41.008.01"
  Set ..GHF = ##class(Test.SendExternalModel.DE1).%New()
  Set ..GHF.DE = "DEX71.41.009.01"
  Set ..ZLF = ##class(Test.SendExternalModel.DE1).%New()
  Set ..ZLF.DE = "DEX71.41.009.01"
  Set ..QTF = ##class(Test.SendExternalModel.DE1).%New()
  Set ..QTF.DE = "DEX71.41.009.01"
  Set ..WDBZ = ##class(Test.SendExternalModel.DE2).%New()
  Set ..WDBZ.DE = "DEX71.41.010.01"
  Set ..GHKSDM = ##class(Test.SendExternalModel.DE2).%New()
  Set ..GHKSDM.DE = "DE08.10.025.00"
  Quit $$$OK
}

2) Have DE1/DE2 extend %SerialObject rather than %RegisteredObject, and define a constructor in DE1. (DE2 can have inheritance simplified a bit, although perhaps not in the context of your full application.) Then, define properties in OPTREGISTER with an InitialExpression - this is passed to the constructor for the serial object. This approach may not actually make sense in the context of your full application.

PropertyParameters:

Class Test.Common.PropertyParameters Extends %XML.PropertyParameters [ ProcedureBlock ]
{

Parameter HDSD As STRING;

Parameter DE As STRING;

Parameter DICLookUp As STRING;

}

DE1:

Class Test.SendExternalModel.DE1 Extends (%SerialObject, %XML.Adaptor) [ PropertyClass = Test.Common.PropertyParameters ]
{

Parameter XMLIGNOREINVALIDTAG = 1;

Parameter XMLIGNORENULL = 1;

Property DE As %String(XMLNAME = "de", XMLPROJECTION = "ATTRIBUTE");

Method %OnNew(pDE As %String = "") As %Status [ Private, ServerOnly = 1 ]
{
  Set ..DE = pDE
  Quit $$$OK
}

Storage Default
{
<Data name="DE1State">
<Value name="1">
<Value>DE</Value>
</Value>
</Data>
<State>DE1State</State>
<StreamLocation>^Test.SendExternalModel.DE1S</StreamLocation>
<Type>%Library.CacheSerialState</Type>
}

}

DE2:

Class Test.SendExternalModel.DE2 Extends Test.SendExternalModel.DE1
{

Property Display As %String(XMLNAME = "display", XMLPROJECTION = "ATTRIBUTE");

Storage Default
{
<Data name="DE2State">
<Value name="1">
<Value>DE</Value>
</Value>
<Value name="2">
<Value>Display</Value>
</Value>
</Data>
<State>DE2State</State>
<StreamLocation>^Test.SendExternalModel.DE2S</StreamLocation>
<Type>%Library.CacheSerialState</Type>
}

}

OPTREGISTER:

Class Test.SendExternalModel.OPTREGISTER Extends (%RegisteredObject, %XML.Adaptor) [ PropertyClass = Test.Common.PropertyParameters ]
{

Parameter XMLIGNOREINVALIDTAG = 1;

Parameter XMLIGNORENULL = 1;

Parameter XMLNAME = "OPTREGISTER";

Property THBZ As Test.SendExternalModel.DE2(DE = "DEX71.41.007.01", DICLookUp = "Y", XMLNAME = "THBZ") [ InitialExpression = "DEX71.41.007.01", Required ];

Property GHFS As Test.SendExternalModel.DE2(DE = "DEX71.41.008.01", DICLookUp = "Y", XMLNAME = "GHFS") [ InitialExpression = "DEX71.41.008.01", Required ];

Property GHF As Test.SendExternalModel.DE1(DE = "DEX71.41.009.01", XMLNAME = "GHF") [ InitialExpression = "DEX71.41.009.01", Required ];

Property ZLF As Test.SendExternalModel.DE1(DE = "DEX71.41.009.01", XMLNAME = "ZLF") [ InitialExpression = "DEX71.41.009.01", Required ];

Property QTF As Test.SendExternalModel.DE1(DE = "DEX71.41.009.01", XMLNAME = "QTF") [ InitialExpression = "DEX71.41.009.01", Required ];

Property WDBZ As Test.SendExternalModel.DE2(DE = "DEX71.41.010.01", DICLookUp = "Y", XMLNAME = "WDBZ") [ InitialExpression = "DEX71.41.010.01" ];

Property GHKSDM As Test.SendExternalModel.DE2(DE = "DE08.10.025.00", DICLookUp = "Y", XMLNAME = "GHKSDM") [ InitialExpression = "DE08.10.025.00", Required ];

}

3) Given all of your original class definitions, add a generator method in OPTREGISTER (or a parent class) that sets the "DE" property of any properties of type DE1/DE2 (or perhaps any properties that have your custom property parameters class) based on the DE property parameter in the containing class's definition. Ensure this method is called prior to XML export. (Might be useful if you need more fine-grained control - i.e., omitting the WDBZ element if it has no content.) This would be a great approach if you have lots of similar classes like these, to avoid repeating (1) every time. A really simple implementation (without any extra sanity checking for property types, etc.), equivalent to the first example, would look like:

Method %OnNew() As %Status [ CodeMode = objectgenerator, Private, ServerOnly = 1 ]
{
  Set tKey = ""
  For {
    #dim tProperty As %Dictionary.CompiledProperty
    Set tProperty = %compiledclass.Properties.GetNext(.tKey)
    If (tKey = "") {
      Quit
    }
    Set tDE = tProperty.Parameters.GetAt("DE")
    If (tDE '= "") {
      Do %code.WriteLine(" Set .."_$$$QN(tProperty.Name)_" = ##class("_tProperty.Type_").%New()")
      Do %code.WriteLine(" Set .."_$$$QN(tProperty.Name)_".DE = "_$$$QUOTE(tDE))
    }
  }
  Do %code.WriteLine(" Quit $$$OK")
  Quit $$$OK
}

In the possible absence of a built-in class for such a purpose, this seems to work:

Class DC.Demo.GlobalReference Extends %String [ ClassType = datatype ]
{

/// 511 is an upper bound for the maximum length of a global reference - see:
/// <a href="https://docs.intersystems.com/latest/csp/docbook/DocBook.UI.Page.cls?KEY=GGBL_structure#GGBL_structure_maxsubscrlen">Maximum Length of a Global Reference</a>
Parameter MAXLEN = 511;

ClassMethod IsValid(%val As %CacheString) As %Status [ ServerOnly = 0 ]
{
    Set tOldZReference = $ZReference
    Set tSC = $$$OK
    Try {
        Set $ZReference = %val
    } Catch e {
        // The above SET will throw a <SYNTAX> exception for an invalid global reference
        Set tSC = $$$ERROR($$$GeneralError,$$$FormatText("Invalid global reference: %1",%val))
    }
    Set $ZReference = tOldZReference
    Quit tSC
}

}

With a dynaForm, this would look like:

Class DC.Demo.ZenLocalizationModel Extends %ZEN.DataModel.ObjectDataModel
{

Property Descrição As %String(ZENATTRS = "requiredMessage:obrigatório.") [ Required ];

}

And:

Class DC.Demo.ZenLocalization Extends %ZEN.Component.page
{

XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen">
<dataController id="myController" modelClass="DC.Demo.ZenLocalizationModel" />
<dynaForm controllerId="myController" injectControls="before">
<submit caption="Enviar" />
</dynaForm>
</page>
}

}

For more information on how to customize data model behavior with property parameters, see the class reference for %ZEN.DataModel.objectModelParameters.

For completeness, here's an extension of the above example that actually works the way you'd expect (using PublicList to handle scope, and New to avoid leaking any variables):

Class DC.Demo.Indirection
{

ClassMethod Driver() [ PublicList = x ]
{
    New x
    Do ..Run()
    Do ..RunSets()
    Do ..Run()
}

ClassMethod Run() [ PublicList = x ]
{
    Set x = 5
    Try {
        Write @"x"
    } Catch e {
        Write e.DisplayString()
    }
    Write !
    Try {
        Xecute "write x"
    } Catch e {
        Write e.DisplayString()
    }
    Write !
}

ClassMethod RunSets() [ PublicList = x ]
{
    Set x = 5
    Set @"x" = 42
    Write x,!
    Xecute "set x = 42"
    Write x,!
}

}

Results:

USER>d ##class(DC.Demo.Indirection).Driver()
5
5
42
42
5
5
 
USER>w

USER>

But that doesn't mean it's a good idea to do things like this.

This can be done with the requiredMessage property of %ZEN.Component.control. There are two ways to accomplish this:

1. Just add the requiredMessage attribute

Class DC.Demo.ZenLocalization Extends %ZEN.Component.page
{

XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen">
<form>
<text label="Descrição" required="true" requiredMessage="obrigatório." />
<submit caption="Enviar" />
</form>
</page>
}

}

Problem is, you'd need to do that in a lot of different places. Instead, you could...

2. Use custom components that subclass built-in control types.

Sample component:

Class DC.Demo.Component.text Extends %ZEN.Component.text [ System = 3 ]
{

/// Feel free to customize this.
Parameter NAMESPACE = "https://community.intersystems.com/post/change-language-required";

/// Value displayed in alert box by the form <method>validate</method>
/// method when this control is required and does not have a value.<br>
/// This is a localized value.
Property requiredMessage As %ZEN.Datatype.caption [ InitialExpression = "obrigatório." ];

}

Sample page using component:

Class DC.Demo.ZenLocalization Extends %ZEN.Component.page
{

XData Contents [ XMLNamespace = "http://www.intersystems.com/zen" ]
{
<page xmlns="http://www.intersystems.com/zen" xmlns:custom="https://community.intersystems.com/post/change-language-required">
<form>
<custom:text label="Descrição" required="true" />
<submit caption="Enviar" />
</form>
</page>
}

}

I was hoping there would be a way to use Zen's localization features to do this, but it seems like that isn't an option, unfortunately;"required." is hard-coded as the InitialExpression for requiredMessage in %ZEN.Component.control, and may only be localized within a page using the component if a non-default value is specified.

Most of the time, you don't really need to use XECUTE/indirection/etc - there is usually a better way to accomplish the same thing. Furthermore, in modern coding practices, XECUTE and name indirection probably will not work the way you expect, because they can only access public variables (not those in the scope of your method). For example, given the following class:

Class DC.Demo.Indirection
{

ClassMethod Run()
{
    Set x = 5
    Try {
        Write @"x"
    } Catch e {
        Write e.DisplayString()
    }
    Write !
    Try {
        Xecute "write x"
    } Catch e {
        Write e.DisplayString()
    }
}

ClassMethod RunSets()
{
    Set x = 5
    Set @"x" = 42
    Write x,!
    Xecute "set x = 42"
    Write x,!
}

}

The output when run in terminal is:

USER>Do ##class(DC.Demo.Indirection).Run()
<UNDEFINED> 9 zRun+3^DC.Demo.Indirection.1 x
<UNDEFINED> 9 zRun+9^DC.Demo.Indirection.1 x
USER>d ##class(DC.Demo.Indirection).RunSets()
5
5
USER>d ##class(DC.Demo.Indirection).Run()
42
42

Your code certainly should not rely on such scoping behavior. There are ways around this (for example, adding  [ PublicList = x ] to the method signature, using New, etc.), but they are messy and difficult to understand and maintain.

In your particular case, using a local array would probably be a better approach - for example:

set x(counter) = DIAGS

Although if you provided more context for why you think you need to dynamically set variables, the community might have other suggestions.