The current chapter in the ongoing function-definition-syntax-saga: In my current implementation, full-form function defintions: ∇ (x) foo (y) { x+y } are global, while short form: ∇ foo {⍺+⍵} are local.
It works, and it's what one wants most of the time.
But man, it's not easily to explain.
Someone on Mastodon suggested ∀ as a prefix for global definitions. It's not too bad, since one could use it for variables too: ∀foo ← 1.
And I still haven't decided on a syntax for operators. I'm looking at making them quite verbose (since defined operators aren't overly common). I want to use defoperator or defop.
@EliasMårtenson "what one wants most of the time" I would definitely never ever want that
@EliasMårtenson imo the syntax for custom functions and custom operators should at least be roughly the same. ∇ left (leftOperand name optionalRightOperand) right { … } isn't that bad
@dzaima (if that's intentional behavior, there should definitely be some actual syntactic difference)
I remember now why I shied away from that approach before: If I parse this: (foo) (bar), then foo could be either a function name or an argument name, depending on what comes next. With the current implementation, seeing an opening paren immediately tells the parser what it is.
I mean, I could just parse the thing as a (possibly-semicolon-separated-list of symbols) and then resolve it once I see the { character.
that's i guess a thing good about my mess of a tokenizer - with how bad it was (its mess of tokenization intertwined with grouping parentheses and statements), I didn't feel too bad squeezing parsing strands in the tokenizer too. :p
(fwiw BQN headers have the advantage that things know their type, so a dyadic operator's header is leftArg LeftOperand _name_ RightOperand rightArg, no parentheses needed)
this pre-grouping of things makes actual parsing (or compilation in the case on BQN) much more simple, and if I didn't do it in the tokenizer, I'd probably have an entire pre-parsing parsing step (but that'd be more inefficient as I'd be duplicating objects often, and 1 object is better than 2 objects)
@dzaima I implemented your suggestion. It can now handle regular function definitions, but with the new more flexible format. This means I'll be able to add support for operator definitions. I'm not proud of the messy code though.
@EliasMårtenson BQN operators (called modifiers) can be "immediate" if they don't contain 𝕨 or 𝕩, which means they evaluate upon getting operands instead of when getting arguments. So Fn ← -{•←"hello" ⋄ 𝕗} (like APL fn ← 5{⎕←'hello' ⋄ ⍺⍺}) prints "hello" before calls to Fn
@dzaima (i guess only this is the actual parsing of it, but it's outsourcing matching parentheses and getting types of things)
@dzaima (canBeImmediate is the boolean telling whether the code contains 𝕨 or 𝕩, but that can be overridden by the header explicitly asking for arguments)
@dzaima (also functions can be immediate too, so •←"a" ⋄ {•←"b" ⋄ •←"c"} ⋄ •←"d" prints all of those. This gets extremely annoying when making side-effect-only functions)
@dzaima (for c.bqn;) "collect" is the part that takes care of logic of what sequences of "tokens" are what (monadic/dyadic call, derivation, train, etc); "tokenize" obviously tokenizes; "compO" converts "tokens" to bytecode through whatever necessary means; "isE" is a part of "collect"; "builtin" converts a character to an instance of the appropriate builtin; "funType" gets function type + immediateness
@dzaima If you know you're going to add all the bits before doing anything else, a better implementation of add() (which, for anyone else following along, places a new bit into a bit vector) would be to keep a 64-bit buffer v, which you append to with v=v*2+(b?1L:0L) (could also shift, but the multiplication is more likely to be fused into an lea instruction). After 64 appends, write it. You don't even have to clear it because the next 64 appends will flush those bits out.
Oh wait, that's for writes with big-endian ordering. Given that I don't think little-endian writes are as fast, you might actually want to walk backwards using that method to write the output.
@Marshall that's a good idea (i was kind of going for as branchless as possible, but that really doesn't make sense when you consider that it's 64-periodic); will play around after ~20 mins
But you could also right-shift v and add the new bit at the left. Getting Java to branchlessly convert a boolean to a top bit might be tricky but is probably possible.
@dzaima I think branch predictors can usually work with length-64 loops, but you might also benchmark length-32 loops as well. I used 32-bit accumulators in Dyalog, but I think that was just for convenience when supporting 32-bit as well as 64-bit architectures. I also used unrolled loops of 8 sometimes, which can be a little faster but not a big difference.
@EliasMårtenson Not worth it because Java will figure it out and make it branchless anyway, or because you can't get Java to consistently make it branchless? In this case, the bit to write can be arbitrary, so branching on each write bit is a huge cost, like a factor of 5 or something if the rest of the code is good.
@Marshall A bit of both. The JVM optimiser has special knowledge of "typical" bytecode generated from the the Java compiler, and has special-cased opimisations for a lot of those cases.
So unless you're ready to benchmark, trying to predict that the optimiser will or will not handle is note really feasible.
@Marshall inlining is the reason I made the r() function for getting array rank - short to type, but can eliminate array bounds checks when the .shape.length is inlined
@Marshall precise branch predictor behavior is probably at the edge of my knowledge, but IIRC (and that's a big if) (much?) longer periods can get 100% prediction too
@dzaima oh, turns out I didn't actually go full-branchless ¯\_(ツ)_/¯
There are certain optimisations that only kick in after a rather significant number of invocations.
And at least as of 10 years ago, the JVM was not able to optimise a running function, so you needed to return from it and call it again for the optimised version to be called. This may have changed though.
> The structure of this formula allows a simple manipulation to generate any desired hexadecimal digit without calculating the previous digits, and each hexadecimal digit is of course just four binary digits.
Also, beware that converting decimal digits to binary is different from converting units. So 0.5 in binary is 0.1, which is different from the 110, which is 5 in binary.
@MartinJaniczek yeah. It's a pretty fundamental operator to not have
@dzaima (though it's a good question what it should do with a function right operand. Any option leads to something being awkward, and I've changed it before and would probably change it again if there was any reason to)
@MartinJaniczek the question is whether or not to include the initial argument in the result array. If you do, then the result array length is one above the number of calls made to f. If you don't, you lose the information of the original argument. (plus the general problem of ⍣ of it calling f at least once guaranteed)
currently I include the initial argument as that seems to be often what I wanted, but it has the strange result that the result will always be at least 2 items
@MartinJaniczek that'd discard the result of precious computation. (really the calculation and checking should be merged, but that's, like, even harder to do)
@MartinJaniczek I've discussed what I know of your application with a couple people here at Dyalog. If you'd like to talk with us, maybe make a presentation to give us a bit more context, then I think we can help determine what might work for your application. Please feel free to contact me via email to set something up!
@MartinJaniczek Even if you don't convince them, it would be useful for us to understand a use case in the real world. If you can't share details, that's fine, just getting a broad understanding of the challenges you face helps us develop frameworks to address them.
@Brian Ah, understood! I think I can give you some broad context about the problem itself and my approach and the difficulties, eg. on a call if you'd like.