Page MenuHomePhabricator

Sum of bytes for a string returns 0
Closed, ResolvedPublicBUG REPORT

Description

Steps to replicate the issue (include links if applicable):

Z14038/sum the elements of a list of natural numbers( Z873/map function( Z24809/number of bytes for code point in UTF-8, Z22717/string to codepoint list("Motörhead")))

The ZObject with the call is below.

{"Z1K1":"Z7","Z7K1":"Z14038","Z14038K1":{"Z1K1":"Z7","Z7K1":"Z873","Z873K1":"Z24809","Z873K2":{"Z1K1":"Z7","Z7K1":"Z22717","Z22717K1":"Motörhead"}}}

What happens?:
The result is 0

What should have happened instead?:
The result should be 10 (I think)

Software version (on Special:Version page; skip for WMF-hosted wikis like Wikipedia):

Other information (browser name/version, screenshots, etc.):

I thought maybe it is because Z873 returns a Z881/typed list(Z1/Object) and Z14038/sum needs a Z881/typed list(Z13518/natural number)

So I created (with the help of 99of9) Z24846/map to natural number, but replacing Z873 with Z24846 in the function call above doesn't make a difference. Here's that one as a ZObject:

{"Z1K1":"Z7","Z7K1":"Z14038","Z14038K1":{"Z1K1":"Z7","Z7K1":"Z24846","Z24846K1":"Z24809","Z24846K2":{"Z1K1":"Z7","Z7K1":"Z22717","Z22717K1":"Motörhead"}}}

Details

Related Changes in GitLab:
TitleReferenceAuthorSource BranchDest Branch
Update to Avro schema version 2.0.0.repos/abstract-wiki/wikifunctions/function-orchestrator!375apineapine-smoltypemain
Customize query in GitLab

Event Timeline

Interesting. When I run this, the result I get is

{
  "Z1K1": "Z13518",
  "Z13518K1": "0[object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object]"
}

This suggests that + is being treated as string concatenation somewhere, probably due to a type conversion fiasco. I'll continue investigating.

Okay, I see what happened. For input lists, we try to read the to-code type converters from the declared input type, rather than from the given type (as we do in the general case). This should improve once we put effort into nested type converters.

For now, I believe we can fix the current case. As long as the provided input contains items of a homogenous type, we can infer the correct type converter by extracting it from the fully-resolved type of any of the list items. Fix underway-ish.

So. The problem here revolves around Z881(Z1) as a type. If the input were instead given as Z881(Z13518), this bug wouldn't arise.

Broadly, I see two ways to solve this. Both ways rely on the inferItemType function, which 1) already exists and 2) can tell us, very reliably, if a Typed List's items are all of the same type and, if so, what that type is.

Solution 1: in our hacky type conversion logic (the part that lets us type-converts the items of typed lists), we could add an additional hack. If we saw that a function's input were of type Z881(Z1) but the list's elements were all of a given type, we could automatically "cast" the list to that type before sending the request to the evaluator, thereby promoting, in this case, the Z881(Z1) as though its type were Z881(Z13518). Again, this is a bit of a hack, but it's all in the part of the code that we'll blow up when/if we get around to nested type converters. This solution may potentially break cases where people are relying on the Z881(Z1) behavior (i.e., no type conversion), but I think that's unlikely.

Solution 2: we could have an explicit promoteListType function that accepts a Z881(Z1), calls inferItemType, and returns a Z881(X), where X is of a more specific type iff the input list contains homogeneously-typed elements. This solution is quite a bit cleaner and relies less on hacks and magic.

I would suggest changing the Map function to return lists of specific types, but, as I understand it, community members have built special workarounds and functions that rely on the present behavior, so let's not do that for now.

@DVrandecic , do you have a preference between solution 1 and solution 2, or another idea for how to solve the problem?

Could that backstop inference work for situations like T394664 too? I understand that would mean mapping the type from input lists to output, but it might save a whole lot of issues.

Please don't change the Map function. You're right, we rely on it for some tricky typing stuff. If you want Typed alternatives, please build them elsewhere first so we can carefully monitor the switch to them.

But do you really want to write one Map function for every Type? (And the community would then need to split all the downstream list functions into one for every type.) Aren't we going to end up with hundreds of user-defined Types?

We appear to have over 140 functions with Z881(Z1) as an argument type (including a handful of builtins). It will take me some time to go through these to assess the impact of the proposed solutions.

Considering solution 1, we already have return Typed list Z18475 that effectively just calls the inferItemType function. It’s not clear to me why wrapping the call to Z873/Map function with Z18475 also returns the Natural number 0.

Investigating further uncovered distinctly odd behaviour.

IMG_1241.png (960×2,079 px, 172 KB)

Even though Z18475 is a no-op, it seems the list it returns (its input) is being interpreted (by inferItemType?) as a list of function calls. This suggests that the input iterable might be a list of function calls rather than their evaluations, but this is not what I would expect from Z873.

Could that backstop inference work for situations like T394664 too? I understand that would mean mapping the type from input lists to output, but it might save a whole lot of issues.

It wouldn't be the same kind of inference. In that case, the input type is fully specified (at least, on the argument itself). The problem is the output type.

We could add yet more dark magic. In this case, it would look like a heuristic: if a function has a list input and returns a non-list output, but the output type is given as Z1, try to type-convert the output as though it were of a type corresponding to that of the input list's elements. My fear is that other things might break when this magic behavior fires off in an unintended context. I'm not necessarily opposed to something like this, but I'd rather think of solutions that let us express the output type directly.

We appear to have over 140 functions with Z881(Z1) as an argument type (including a handful of builtins). It will take me some time to go through these to assess the impact of the proposed solutions.

Considering solution 1, we already have return Typed list Z18475 that effectively just calls the inferItemType function. It’s not clear to me why wrapping the call to Z873/Map function with Z18475 also returns the Natural number 0.

Investigating further uncovered distinctly odd behaviour.

IMG_1241.png (960×2,079 px, 172 KB)

Even though Z18475 is a no-op, it seems the list it returns (its input) is being interpreted (by inferItemType?) as a list of function calls. This suggests that the input iterable might be a list of function calls rather than their evaluations, but this is not what I would expect from Z873.

To be clear, the issue isn't when the input type is declared in Z17 as Z881(Z1). The issue is when the actual argument supplied declares itself as being of type Z881(Z1), but it has homogeneously-typed elements of some other type X. Another thing we might consider here is a Boolean flag (perhaps on the Z17 itself) that says, "If my input type is underspecified (i.e., involves Z1), but the instantiated argument is of a more specific type, pretend for all purposes that this function was declared as accepting that type." That would at least let us control when some hazily-defined heuristic behaviors take place.

But again, I think it's cleaner if we work toward a situation where these functions are themselves produced by another function, i.e. where we don't just have a function

function doSomethingWithAList( listInput: Z881(Z1) ) { ... }

but where we have a function that returns a type-specialized function, e.g.

function getDoSomethingWithAListFunction( elementType: X ) {
    return doSomethingWithAList( listInput: Z881(X) ) { ... }
}

Please don't change the Map function. You're right, we rely on it for some tricky typing stuff. If you want Typed alternatives, please build them elsewhere first so we can carefully monitor the switch to them.

But do you really want to write one Map function for every Type? (And the community would then need to split all the downstream list functions into one for every type.) Aren't we going to end up with hundreds of user-defined Types?

Agreed on both points. I definitely don't plan to mess with Map :).

But I do think we could make a new version of Map that is type-parameterized (see my comment above). So not writing a separate Map function for each type, but writing a function that creates Map functions, parameterized by a type. Edit: this function could even try to infer the types from the signature of the Function provided as input, instead of being provided an explicit type argument.

We appear to have over 140 functions with Z881(Z1) as an argument type (including a handful of builtins). It will take me some time to go through these to assess the impact of the proposed solutions.

Considering solution 1, we already have return Typed list Z18475 that effectively just calls the inferItemType function. It’s not clear to me why wrapping the call to Z873/Map function with Z18475 also returns the Natural number 0.

Investigating further uncovered distinctly odd behaviour.

IMG_1241.png (960×2,079 px, 172 KB)

Even though Z18475 is a no-op, it seems the list it returns (its input) is being interpreted (by inferItemType?) as a list of function calls. This suggests that the input iterable might be a list of function calls rather than their evaluations, but this is not what I would expect from Z873.

I wasn't aware of Z18475; it's clever. I don't know why this is happening this way, either. Are you saying that Z18475 is already wrapping the argument to sum? If so, I can dig further into this.

Edit: I see no evidence that Z18475 is getting called. Can you wire the functions back up so I can reproduce what you're seeing?

[…snip…]

Edit: I see no evidence that Z18475 is getting called. Can you wire the functions back up so I can reproduce what you're seeing?

Yes. Per the original Description, this was an ad-hoc function call but I’ve created it as a test case for the sum function (all implementations fail or return an incorrect result). I’ve also created a test for Z18475 with an incorrect expected result. The actual result given there is missing a "Z881":

{ "Z1K1": "Z7", "K1": { "Z1K1": "Z13518", "Z13518K1": "1" }, "K2": [ "Z13518", { "Z1K1": "Z13518", "Z13518K1": "1" }, { "Z1K1": "Z13518", "Z13518K1": "1" }, { "Z1K1": "Z13518", "Z13518K1": "2" }, { "Z1K1": "Z13518", "Z13518K1": "1" }, { "Z1K1": "Z13518", "Z13518K1": "1" }, { "Z1K1": "Z13518", "Z13518K1": "1" }, { "Z1K1": "Z13518", "Z13518K1": "1" }, { "Z1K1": "Z13518", "Z13518K1": "1" } ] }

Good(-ish?) news! When I run your test case locally (as of orchestrator HEAD), I get the correct result. My function call looks like

{
    "Z1K1": "Z7",
    "Z7K1": "Z14038",
    "Z14038K1": {
        "Z1K1": "Z7",
        "Z7K1": "Z18475",
        "Z18475K1": {
            "Z1K1": "Z7",
            "Z7K1": "Z873",
            "Z873K1": "Z24809",
            "Z873K2": {
                "Z1K1": "Z7",
                "Z7K1": "Z22717",
                "Z22717K1": "Motörhead"
            }
        }
    }
}

and my output looks like

{
  'Z1K1': 'Z22',,
  'Z22K1': {
    'Z1K1': 'Z13518', 'Z13518K1': '10'},
    ...
  }
}

When I run as of the currently productionized version of the orchestrator, I get the 0 result. This tells me that the work we've been doing on type representation in the orchestrator -> evaluator request will fix this issue. Happy to keep discussing ideas for ways to make type specifications more elegant, perhaps in another venue.

For now, I'll move this bug to "Ready to deploy." After our next deployment (Wednesday of next week), I'd expect the test cases you listed to begin passing. If they don't, I'll re-open the investigation :).

Ah, that’s excellent news, thank you!

I have a few thoughts on the broader topic but I’ll save them for after the next deployment. Were you thinking of an on-wiki venue or a Feature request?

Ah, that’s excellent news, thank you!

I have a few thoughts on the broader topic but I’ll save them for after the next deployment. Were you thinking of an on-wiki venue or a Feature request?

A feature request, please!

But again, I think it's cleaner if we work toward a situation where these functions are themselves produced by another function, i.e. where we don't just have a function

function doSomethingWithAList( listInput: Z881(Z1) ) { ... }

but where we have a function that returns a type-specialized function, e.g.

function getDoSomethingWithAListFunction( elementType: X ) {
    return doSomethingWithAList( listInput: Z881(X) ) { ... }
}

The only attempt like this that I know about is https://www.wikifunctions.org/wiki/Z10249, but we haven't got it to work, see https://www.wikifunctions.org/view/en/Z17499. Any chance you can show us an example of how to do it?

Ah! To be clear, this won't work yet. I don't know exactly why. I'll check out Z10249.

Run from the server just now:

$ curl https://wikifunctions.k8s-staging.discovery.wmnet:30443/1/v1/evaluate --data '{"zobject":{"Z1K1":"Z7","Z7K1":"Z14038","Z14038K1":{"Z1K1":"Z7","Z7K1":"Z873","Z873K1":"Z24809","Z873K2":{"Z1K1":"Z7","Z7K1":"Z22717","Z22717K1":"Motörhead"}}}}' -H "Content-type: application/json"

{"Z1K1":"Z22","Z22K1":{"Z1K1":"Z13518","Z13518K1":"0[object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object]"},"Z22K2":{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z883","Z883K1":"Z6","Z883K2":"Z1"},"K1":[{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"wasmedgeTotalExecutionTime","K2":"103269036"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"wasmedgeGasCost","K2":"5814"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"executionMemoryUsage","K2":"0.91 MiB"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"executionCpuUsage","K2":"170 μs"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"programmingLanguageVersion","K2":"QuickJS v0.5.0-alpha"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationMemoryUsage","K2":"106.16 MiB"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationCpuUsage","K2":"16.798 ms"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationStartTime","K2":"2025-06-10T14:02:48.359Z"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationEndTime","K2":"2025-06-10T14:02:48.373Z"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationDuration","K2":"14 ms"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationHostname","K2":"function-evaluator-javascript-evaluator-66bbcb5d6-srbfv"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"implementationId","K2":{"Z1K1":"Z6","Z6K1":"Z14039"}},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"implementationType","K2":"Z14K3"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationMemoryUsage","K2":"141.26 MiB"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationCpuUsage","K2":"1006.586 ms"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationStartTime","K2":"2025-06-10T14:02:46.988Z"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationEndTime","K2":"2025-06-10T14:02:48.410Z"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationDuration","K2":"1422 ms"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationHostname","K2":"function-orchestrator-main-orchestrator-dd5798f78-dc6t6"}]}}jforrester@deploy1003:/srv/deployment-charts/helmfile.d/services/wikifunctions$

… which doesn't look right at all.

Run from the server just now:

$ curl https://wikifunctions.k8s-staging.discovery.wmnet:30443/1/v1/evaluate --data '{"zobject":{"Z1K1":"Z7","Z7K1":"Z14038","Z14038K1":{"Z1K1":"Z7","Z7K1":"Z873","Z873K1":"Z24809","Z873K2":{"Z1K1":"Z7","Z7K1":"Z22717","Z22717K1":"Motörhead"}}}}' -H "Content-type: application/json"

{"Z1K1":"Z22","Z22K1":{"Z1K1":"Z13518","Z13518K1":"0[object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object][object Object]"},"Z22K2":{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z883","Z883K1":"Z6","Z883K2":"Z1"},"K1":[{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"wasmedgeTotalExecutionTime","K2":"103269036"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"wasmedgeGasCost","K2":"5814"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"executionMemoryUsage","K2":"0.91 MiB"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"executionCpuUsage","K2":"170 μs"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"programmingLanguageVersion","K2":"QuickJS v0.5.0-alpha"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationMemoryUsage","K2":"106.16 MiB"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationCpuUsage","K2":"16.798 ms"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationStartTime","K2":"2025-06-10T14:02:48.359Z"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationEndTime","K2":"2025-06-10T14:02:48.373Z"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationDuration","K2":"14 ms"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"evaluationHostname","K2":"function-evaluator-javascript-evaluator-66bbcb5d6-srbfv"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"implementationId","K2":{"Z1K1":"Z6","Z6K1":"Z14039"}},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"implementationType","K2":"Z14K3"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationMemoryUsage","K2":"141.26 MiB"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationCpuUsage","K2":"1006.586 ms"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationStartTime","K2":"2025-06-10T14:02:46.988Z"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationEndTime","K2":"2025-06-10T14:02:48.410Z"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationDuration","K2":"1422 ms"},{"Z1K1":{"Z1K1":"Z7","Z7K1":"Z882","Z882K1":"Z6","Z882K2":"Z1"},"K1":"orchestrationHostname","K2":"function-orchestrator-main-orchestrator-dd5798f78-dc6t6"}]}}jforrester@deploy1003:/srv/deployment-charts/helmfile.d/services/wikifunctions$

… which doesn't look right at all.

This is because of the input type issue: the input declares its type as Z881(Z1) but contains objects of a specific type. I believe it will work if you instead do

curl https://wikifunctions.k8s-staging.discovery.wmnet:30443/1/v1/evaluate --data '{"zobject":{"Z1K1":"Z7","Z7K1":"Z14038","Z14038K1":{"Z1K1":"Z7","Z7K1":"Z873","Z873K1":"Z24809","Z873K2":{"Z1K1":"Z7","Z7K1":"Z18475","Z18475K1":{"Z1K1":"Z7","Z7K1":"Z22717","Z22717K1":"Motörhead"}}}}}' -H "Content-type: application/json"

Mmmmmmm, nope. That didn't work, either. Turns out that Z18475 isn't fixing the problem because it doesn't know how to type-convert the resulting list.

Basically, we need a Map function that declares its input and output types. Even nested type conversion can't fix this case, unfortunately.

Hmm, okay, there are some more alternatives.

  1. If we really wanted to get hacky, we could have Z18475 avoid both input and output type conversion. Then the inputs to Z18475 wouldn't be type-converted, and we'd avoid the error we get with the incantation above.
  1. We could make a builtin version of Z18475. That would infer the input type but avoid type conversion (since it would never hit the evaluator).

@Jdforrester-WMF @DVrandecic , any thoughts?

WAIT. Never mind, I wasn't thinking straight. This should work:

curl https://wikifunctions.k8s-staging.discovery.wmnet:30443/1/v1/evaluate --data '{"zobject":{"Z1K1": "Z7", "Z7K1": "Z14038", "Z14038K1": {"Z1K1": "Z7", "Z7K1": "Z18475", "Z18475K1": {"Z1K1": "Z7", "Z7K1": "Z873", "Z873K1": "Z24809", "Z873K2": {"Z1K1": "Z7", "Z7K1": "Z22717", "Z22717K1": "Motörhead"}}}}}' -H "Content-type: application/json"

When I run this, I get the expected 10.

#literalshowerthoughts

To be clear, the above is still a nasty kludge. We should consider doing something to help us manipulate types more luculently.

That said, I think we can close this one, since it technically can be made to work.

Do we need to alert community members about this kludge and how they might use it?

I'll send a missive in IRC. Will also ask Luca how else to communicate this. I will close this task, though, since it's "working" (+kludge).