Conditional debugging idiom?

I found myself writing stuff like

if(~debugSomething.notNil) { something.postln }

when debugging my extensions to the class library. The above bit is actually a bit buggy itself when a ProxySpace is entered, because then every environment variable is not nil. So if(~debugSomething == 1) seems a bit safer. There’s also the issue that in scalng startup context, currentEnvironment is nil for a while, so the above idiom can also throw an error in such a context. A safer version gets more “wordy” as

if(currentEnvironment.notNil)  { if(~debugSomething == 1) { /* post */ } }

I can’t be first person to need a conditional debugging facility like this, but I can’t seem to find one already in sclang. Object.debug isn’t conditional on something. So, am I reinventing a wheel here or is there some facility that does the same e.g. in some quark that I haven’t looked at?

if(currentEnvironment.notNil) { if(~debugSomething == 1) { /* post */ } }

How can currentEnvironment ever be nil??

You know you can instead of pushing your ProxySpace simply instantiate a
new one:

p = ProxySpace.new(s);
p[\someKey] = {...};

… and you’re not gonna run into conflicts with your environment variables.

Actually, you can even make currentEnvironment = nil explicitly after sclang has fully loaded.

e = currentEnvironment 
currentEnvironment = nil
~bohuuhu // ERROR: Message 'at' not understood. RECEIVER:  nil

currentEnvironment = e

No need for the more complicated setup below.

Try inserting code that looks up e.g. ~thisCanHappen in ControlName.new

+ ControlName {

	*new { arg name, index, rate, defaultValue, argNum, lag;
		~thisCanHappen; // next line is unchanged
		^super.newCopyArgs(name.asSymbol, index, rate, defaultValue, argNum, lag ? 0.0)
	}
}

and you’ll see that it can be the case that that throws an error due being called with
currentEnvironment=nil.

Well, here’s what I get with your example after recompiling:

compile done
localhost : setting clientID to 0.
internal : setting clientID to 0.
ERROR: Message 'at' not understood.
RECEIVER:
   nil
ARGS:
   Symbol 'thisCanHappen'

PROTECTED CALL STACK:
	Meta_MethodError:new	0x562542cd9a40
		arg this = DoesNotUnderstandError
		arg what = nil
		arg receiver = nil
	Meta_DoesNotUnderstandError:new	0x562542cdbd80
		arg this = DoesNotUnderstandError
		arg receiver = nil
		arg selector = at
		arg args = [ thisCanHappen ]
	Object:doesNotUnderstand	0x562542860940
		arg this = nil
		arg selector = at
		arg args = nil
	Meta_ControlName:new	0x562541088a00
		arg this = ControlName
		arg name = bufnum
		arg index = 0
		arg rate = control
		arg defaultValue = 0
		arg argNum = 0
		arg lag = 0
	SynthDef:addKr	0x5625473a1a40
		arg this = SynthDef:grain
		arg name = bufnum
		arg values = 0
		arg lags = 0
	ArrayedCollection:do	0x562541acae80
		arg this = SymbolArray[ bufnum, t_trig, start, end, out, rate, tempo, atk, sust, rel, curve, gate, grainAmp ]
		arg function = a Function
		var i = 0
	SynthDef:addControlsFromArgsOfFunc	0x56254739ecc0
		arg this = SynthDef:grain
		arg func = a Function
		arg rates = [ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ]
		arg skipArgs = 0
		var def = < closed FunctionDef >
		var names = SymbolArray[ bufnum, t_trig, start, end, out, rate, tempo, atk, sust, rel, curve, gate, grainAmp ]
		var values = [ 0, 0, 0, 1, 0, 1, 1, 0.1, 1, 0.7, -4, 1, 1.0 ]
		var argNames = SymbolArray[ bufnum, t_trig, start, end, out, rate, tempo, atk, sust, rel, curve, gate, grainAmp ]
		var specs = nil
	SynthDef:buildUgenGraph	0x56254739e740
		arg this = SynthDef:grain
		arg func = a Function
		arg rates = nil
		arg prependArgs = [  ]
		var result = nil
		var saveControlNames = nil
	a FunctionDef	0x56254739cd80
		sourceCode = "<an open Function>"
	Function:prTry	0x562545695680
		arg this = a Function
		var result = nil
		var thread = a Thread
		var next = nil
		var wasInProtectedFunc = false
	
CALL STACK:
	DoesNotUnderstandError:reportError
		arg this = <instance of DoesNotUnderstandError>
	Nil:handleError
		arg this = nil
		arg error = <instance of DoesNotUnderstandError>
	Thread:handleError
		arg this = <instance of Thread>
		arg error = <instance of DoesNotUnderstandError>
	Object:throw
		arg this = <instance of DoesNotUnderstandError>
	Function:protect
		arg this = <instance of Function>
		arg handler = <instance of Function>
		var result = <instance of DoesNotUnderstandError>
	SynthDef:build
		arg this = <instance of SynthDef>
		arg ugenGraphFunc = <instance of Function>
		arg rates = nil
		arg prependArgs = nil
	Meta_AbstractSNSampler:initClass
		arg this = <instance of Meta_AbstractSNSampler>
	Meta_Class:initClassTree
		arg this = <instance of Meta_Class>
		arg aClass = <instance of Meta_AbstractSNSampler>
		var implementsInitClass = nil
	ArrayedCollection:do
		arg this = [*739]
		arg function = <instance of Function>
		var i = 156
	Meta_Class:initClassTree
		arg this = <instance of Meta_Class>
		arg aClass = <instance of Meta_Object>
		var implementsInitClass = nil
	Process:startup
		arg this = <instance of Main>
		var time = 130.598814166
	Main:startup
		arg this = <instance of Main>
		var didWarnOverwrite = false
^^ ERROR: Message 'at' not understood.
RECEIVER: nil

I don’t know what you’re trying to achieve but I dare to say it’s conceptually wrong. I’ve been using SuperCollider for some 16 years or so now and I still love it for its elegance and its basic concepts. Though I’m no programmer I’ve been able to write complex programs which I’m still using. I’d say don’t try to work against those concepts - it’s all quite nicely laid out and you can achieve a lot if you follow what’s already there. Sorry, I think I can’t help with that problem.

If you want to create your own environment and use that instead of what’s in currentEnveronment simply you can create your own:

e = Environment.make({ ~test = 42 });
e.push;
currentEnvironment; // -> Environment[ (test -> 42) ]
e.pop;
currentEnvironment; // -> Environment[  ]

Don’t declare currentEnvironment = nil;. You can have as many environments as you like and you can easily switch back and forth between them.

If anybody is curious, in the default setup, with no quarks installed whatsoever, currentEnvironment is never nil during sclang startup calls to ControlName.new.

I found one of the buggy (??) quarks that causes that currentEnvironment=nil to happen: AudacityLabels. There are probably others. It’s not a sclang promise that currentEnvironment is not nil so it’s not clear if those quarks can be declared buggy merely because they do that.

A number of quarks that are safe in that regard, i.e. don’t make currentEnvironment = nil during startup: XML, JITLibExtensions, TabbedView2.

If there’s any quark that makes currentEnvironment return nil then it’s a bug. That should never happen.

Using your own, self-defined environment in your classes is a valid solution. If you do it right these environments should be perfectly protected from conflicting with other environments (especially if your declaring variables like ~myVar = some_value in your other, non-class code).

“mmExtensions” also makes that happen during sclang startup. Neither of those two “buggy” quarks (the other being AudacityLabels) will nil you environment later, during normal use.

By the way, friendlier code to check that, meaning it won’t crash your interpreter:

+ ControlName {

	*new { arg name, index, rate, defaultValue, argNum, lag, preMangled = false;
		if(currentEnvironment.isNil) { "That's crazy!".postln }
		^super.newCopyArgs(
			if(preMangled) { name.asSymbol } { name.asSymbol.mangle }, // short-circuits
			index, rate, defaultValue, argNum, lag ? 0.0)
	}
}

I wouldn’t rely too much on exotic quarks. Quarks hardly get reviewed, opposite to contributions to core classes. They may always cause troubles.

Well, you were apparently able to reproduce this, so you are using one the quarks that is buggy in that respect… It’s the one that calls Meta_AbstractSNSampler:initClass in your case. That’s the SNSamplePlayer class, but I’m not sure where the quark is located.

I was able to reproduce… what? Just a note on SNSamplerPlayer: I wouldn’t advertise this in a quark. It’s a highly experimental library. There may certainly be bugs lurking there. But I’m not aware it’s ever making currentEnvironment return nil (nor have I willingly made currentEnvironment return nil).
One explanation why currentEnvironment is returning nil is that Environment hasn’t been initialized before calling currentEnvironment. That’s why there’s the initClassTree method in Class - that should guarantee that a certain class that’s needed in your class will be accessible when your class gets initialized on boot or reboot:
https://pustota.basislager.org/_/sc-help/Help/Classes/Class.html#*initClassTree

Yes, I see now it’s your own quark https://github.com/nuss/SNSampler or rather just extension, since there’s no .quark file.

As far as common debugging idioms go, something I’ve encountered a lot in the class library and several Quarks I’ve looked at is the usage of a “verbose” flag (either passed as additional argument to certain class methods or as a classvar, and usually set to false by default) - I’m guessing you’re trying to use an environment variable as a sort of “global” debug setting (which wouldn’t be possible with the method I mentioned)?

SomeClass {
	classvar <>verbose = false;
	// ...
	someMethod {
		if(verbose) {
			// print debug info
		};
		// ...
	}
}

+ SomeOtherClass {
	someOtherMethod { |verbose = false|
		if(verbose) {
			// print debug info
		};
		// ...
	}
}

Unfortunately adding classvars to exisiting classes is not possible from + class extensions (only methods can be added to existing classes that way), but one could as a hack add another class that hosts the debug classvar. I’ve done that recently for a more serious storage (cache) bit.

Just tested, on my machine with AudacityLabels installed, currentEnvironment is not set to nil on startup (or at any other time).

I just checked it again. As the only quark. Without it no “that’s crazy” messages. Buit with it

compile done
localhost : setting clientID to 0.
internal : setting clientID to 0.
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
That's crazy!
Class tree inited in 0.02 seconds

Here’s a back trace, showing that it’s indeed AudacityLabels doing this:

That's crazy!
CALL STACK:
	Meta_ControlName:new
		arg this = <instance of Meta_ControlName>
		arg name = '?'
		arg index = 6
		arg rate = '?'
		arg defaultValue = 0.10000000149012
		arg argNum = nil
		arg lag = nil
		arg preMangled = false
	< FunctionDef in Method Meta_Collection:fill >
		arg i = 6
	Integer:do
		arg this = 7
		arg function = <instance of Function>
		var i = 6
	Meta_Collection:fill
		arg this = <instance of Meta_Array>
		arg size = 7
		arg function = <instance of Function>
		var obj = [*6]
	< FunctionDef in Method SynthDesc:readSynthDef2 >  (no arguments or variables)
	Function:prTry
		arg this = <instance of Function>
		var result = nil
		var thread = <instance of Thread>
		var next = nil
		var wasInProtectedFunc = false
	Function:protect
		arg this = <instance of Function>
		arg handler = <instance of Function>
		var result = nil
	SynthDesc:readSynthDef2
		arg this = <instance of SynthDesc>
		arg stream = <instance of CollStream>
		arg keepDef = true
		var numControls = 7
		var numConstants = 8
		var numControlNames = nil
		var numUGens = nil
		var numVariants = nil
	SynthDescLib:readDescFromDef
		arg this = <instance of SynthDescLib>
		arg stream = <instance of CollStream>
		arg keepDef = true
		arg def = <instance of SynthDef>
		arg metadata = <instance of Event>
		var desc = nil
		var numDefs = 1
		var version = 2
	SynthDef:asSynthDesc
		arg this = <instance of SynthDef>
		arg libname = 'global'
		arg keepDef = true
		var lib = <instance of SynthDescLib>
		var stream = <instance of CollStream>
	SynthDef:add
		arg this = <instance of SynthDef>
		arg libname = nil
		arg completionMsg = nil
		arg keepDef = true
		var servers = nil
		var desc = nil
	Meta_LabeledSoundFile:initClass
		arg this = <instance of Meta_LabeledSoundFile>
	Meta_Class:initClassTree
		arg this = <instance of Meta_Class>
		arg aClass = <instance of Meta_LabeledSoundFile>
		var implementsInitClass = nil
	ArrayedCollection:do
		arg this = [*2]
		arg function = <instance of Function>
		var i = 1
	Meta_Class:initClassTree
		arg this = <instance of Meta_Class>
		arg aClass = <instance of Meta_AbstractAudacityLabels>
		var implementsInitClass = nil
	ArrayedCollection:do
		arg this = [*203]
		arg function = <instance of Function>
		var i = 49
...
Class tree inited in 0.03 seconds

The call with nil currentEnvironment is through Meta_LabeledSoundFile:initClass.

I’m using the current version of the quark on SC 3.12.1.

And I think it’s easy to see how it happens too. AudacityLabels does this:

	*initClass {

		Class.initClassTree(SynthDescLib);

		SynthDef(\labelPlayer_1, { |out = 0, rate = 1, t0, t1, buffer, pan, amp = 0.1|

at which point some ControlNames are going to be ultimately created by compiling those synths to graphs. But currentEnvironment is set for the first time (I think) in an instance (not class) method of Process:

Process {
	// A Process is a runtime environment.
	var classVars, <interpreter;
	var curThread, <mainThread;
	var schedulerQueue;
	var <>nowExecutingPath;

	startup {
		var time;

		Class.initClassTree(AppClock); // AppClock first in case of error
		time = this.class.elapsedTime;
		Class.initClassTree(Object);
		("Class tree inited in" + (this.class.elapsedTime - time).round(0.01) + "seconds").postln;
		Class.classesInited = nil;

		topEnvironment = Environment.new;
		currentEnvironment = topEnvironment;

So that can’t possibly happen while (any) classes are still being initialized.

Which also gives us a much simpler reproducer:

ForTheSkeptics {

	*initClass {

		Class.initClassTree(Process); // doesn't help, obviously

		postln("currentEnvironment is:" + currentEnvironment);

	}
}

Runs as:

compile done
currentEnvironment is: nil
localhost : setting clientID to 0.
internal : setting clientID to 0.
Class tree inited in 0.02 seconds

The solution, then, is for AudacityLabels to delete the initClassTree and instead do:

StartUp.add {
    SynthDef(...).add;
    // etc
}

… within its *initClass method.

hjh

1 Like

Thanks for the tip, James. I was wondering how one can do the latter, i.e. run after the whole tree is init.

By the way, it turns out the issue with AudaciyLabels’ init was noted already by someone else a year ago https://github.com/musikinformatik/AudacityLabels/issues/1