(dev version) New keyword-args features vs old-style dictionary object prototyping

Years ago, I used object-prototyping to write a pretty nifty graphical demo of, basically, oversampling.

I’ve also updated my dev branch, including the kwargs changes.

The demo no longer works.

Demo code: Interactive oversampling demo · GitHub

The issue is occurring in the data_ function, where array is receiving a single 0. It’s supposed to receive an array: data.data = Array.fill(size, 0);.

I think the kwargs stuff is mistakenly distributing the array values across argument names (but there’s only one, so most of the array gets discarded).

I have to admit that I’m a bit lost on all the ins-and-outs of the kwargs change. If it’s necessary to break compatibility, then users should be given some guidelines on how to update their old code. But it would be better not to break compatibility, I think.

Relevant top of stack trace:

ERROR: Container got 0 values, expected 32

PROTECTED CALL STACK:
	a FunctionDef	0x56159bfc7368
		sourceCode = "<an open Function>"
		arg self = ('upsampleShift_': a Function, 'data_': a Function, 'fft_': a Function, 'upsampleShift': 0, 
  'syncShifted': a Function)
		arg array = 0  <<-- here, this should be an array
	a FunctionDef	0x5615987edd40
		sourceCode = "<an open Function>"
		arg func = a Function
	Object:!?	0x561592782500
		arg this = a Function
		arg obj = a Function
	IdentityDictionary:doesNotUnderstand	0x5615987ed8c0
		arg this = ('upsampleShift_': a Function, 'data_': a Function, 'fft_': a Function, 'upsampleShift': 0, 
  'syncShifted': a Function)
		arg selector = data_
		arg args = nil
		arg kwargs = nil

Not logging as a bug yet because I’m not sure if the policy going forward is that this type of code needs to be edited.

hjh

MWE:

e = (x: { |self, data| data });

e.x([1, 2, 3]);
-> 1  // uh... no

EDIT: I’m going to have to put this down now, but it looks to me like there is some confusion between performArgs and functionPerformList.

f = { |a, b| (a: a, b: b) };

f.functionPerformList(\value, 1, 2)  ... ?
-> ('a': 1, 'b': 2)

f.functionPerformList(\value, [1, 2])
-> ('a': 1, 'b': 2)

f.performArgs(\functionPerformList, [\value, 1, 2]);  ... ?
-> ('a': 1, 'b': 2)

I’m already lost: performArgs is undocumented, so I can only guess at the correct format of the array. Should the args be array-wrapped or not?

f.performArgs(\functionPerformList, [\value, [1, 2]]);
-> ('a': 1, 'b': 2)

So, array-wrapped and not-array-wrapped behave the same. Not sure if that’s expected.

So then the only way to pass an array through is to wrap the array.

f.performArgs(\functionPerformList, [\value, [[1, 2]]]);
-> ('a': [1, 2])

I guess this means that the fix would be: do NOT do [\value, this] ++ args, instead write [\value, this, args] because ++ removes a layer of array wrapping, which the primitives depend on. I tried this and it does get the expected result. I’m just rather confused and not sure if my reasoning is sound.

That is – I would have assumed that |... args| in functionPerformList would reintroduce the array wrapping that ++ removed – but this isn’t the case. So I’m genuinely not certain if the right solution is just to extra-wrap it, or if the primitives are not doing what we’re expecting. (And apologies if I’m just being dense.)

Got a project due in 2 days, so I won’t do any more with this for a little while – I’ll revert back to stable until it’s done.

hjh

That’s wrong, I’m surprised the tests didn’t catch this because there was quite a few. I’ll have a look later today.

PerformArgs, takes a selector, an array of args, and an array of keyword-arg pairs. I thought I did the documentation, but guess I forgot, I’ll do that too.

FunctionPerformList always expands the last array. PerformArgs just passes the args and keyword-args along.

Yes I think this sounds right. The performList methods are eternally confusing.

1 Like

Thanks for this, your fix was correct

Interestingly the following would have worked.

e = (x: { |self, data| data });
e.x([1, 2, 3], []); // [1, 2, 3]

Cool, thanks for confirming that. I was really not very clear on what structure is supposed to be passed where.

:face_with_raised_eyebrow: A pretty tangled mess, then.

hjh

In hindsight, I think it would have been better to make performlist a bytecode as it’s only needed because of *[], which the user shouldn’t really be overriding.

Just to be clear about why this wasn’t found, this issue only arises when using performArgs with (function/super)perfromList, and the final element in the args is either of class Array or List.

What if a user doesn’t like Event-style object prototyping and wants to create their own Proto object for that? (… which… I did… and have a two-decades body of work depending on it…)

If performList were frozen into the interpreter and impossible to override, how would one create an alternate object prototyping strategy? Ok, that’s really doesNotUnderstand though. But my Proto does handle perform* too.

I suspect perform and variants come right from Smalltalk. At least, I don’t object to the flexibility. Smalltalk never really caught on so its “everything is an object, every operation is a method” is still an outlier in the field of programming languages.

hjh

performList is only need to take the last argument, and unpack it if it is an array. performList does not mean take the array and use it’s elements as arguments. Although it overlaps if the only argument is an array.

What I’m saying is if the array unpacking was a bytecode you could just use perform everywhere.

I don’t know why you’d override perform list on X, as it creates difference beyond unpacking the array between X().foo(*[...]) and X().foo(...). I don’t think prototyping has anything to do with this.

Also if it was a bytecode I think you could do structured bindings better, as it would just mean, take this array, put it’s contents on to the stack. Eg,

var head1, head2... tail = *[...];

Yeah, I was a bit muddled writing that. Sorry. Indeed, perform should be the place to override.

That’s a very interesting idea. I guess someone at some point would try to write var a, b = 2 ... c = *[10, 20, 30]… syntax error, or c takes [10, 20, 30]?

One thing about “put things onto the stack” is that subsequent operations need to know how many things were put on the stack. SendMsg with 15 or fewer args (including receiver) encode the number of stack items in the byte code: A1 = there’s only a receiver, A2 = receiver + one arg, etc, up to AF = receiver + 14 args. Receiver + 15 args goes to 0A (SendMsgX) with a second byte for the number of stack items.

{ x.flapdoodle(1, 2, \symbol, 81, 777) }.def.dumpByteCodes
 11   A6 00    SendMsg 'flapdoodle'

{ x.flapdoodle(1, 2, \symbol) }.def.dumpByteCodes
  6   A4 00    SendMsg 'flapdoodle'

{ x.flapdoodle(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1) }.def.dumpByteCodes
 18   0A 10 00 00 SendMsgX 'flapdoodle'

So in var a, b, c = *[10, 20, 30], you’d push 10, 20, 30 onto the stack in that order. Then a needs to skip back 2 on the stack, b skips back 1, and c gets the head. So you’d have to reverse-assign.

Size may be variable – “why would one do that?” but it would be legal:

var stuff = Array.fill(rrand(10, 30), { 1.0.rand });
var a, b ... c = *stuff;

(Edit: Or, more realistic, stuff is an argument, whose value the functiondef doesn’t have any control over.)

Now the compiler doesn’t know how many stack items to handle (how many items to give c). It’s a very cool idea, but not without design/implementation hurdles.

OK, back to that project (I can’t help myself lol).

hjh

I think this would have been surmountable back when SC was younger, you could have a separate stack that lists the number of args for all calls (for example). I don’t think it’s realistic right now.

At some point I want to refactor the bytecode parts of the codebase, removing the magic numbers from the parser, and hopefully removing one bytecode, so we can have an extended set of bytecodes. I’d really like to add type based stuff as I’ve got a draft of method overloading on the go.

Haven’t checked in on Hadron lately – having just seen SC hallucinate a 1.put(aTempoClock) call that I know I didn’t write, never happened before or since, I’m reminded that our interpreter isn’t bulletproof and it would be great to have another engine.

hjh

1 Like