toot.cat is one of the many independent Mastodon servers you can use to participate in the fediverse.
On the internet, everyone knows you're a cat — and that's totally okay.

Administered by:

Server stats:

476
active users

Public

I seem to have gotten a very basic qualified type thing working. still need to noodle with it more, though.

Public

Qualified types introduce a type predicate that needs to be checked after inferring each expression. For example, let's say the signature of '+' is 'a, a -> a' where 'a' is any type. Addition only makes sense for certain types of data, so we can then specify which types are valid with a predicate. For example: where 'a' is a float or an int. Adding two booleans is valid as far as the function signature is concerned, but it will then fail the predicate check.

Public

One limitation of this approach seems to be (meaning I haven't found a paper/article stating otherwise yet) that it cannot express all of the possibilities of function overloading. For example, in GLSL it is valid to multiply a matrix by a vector. It is also valid to multiply a vector by a matrix. Both forms return a matrix, but the signatures are different. The first is 'a, b -> a' and the second is 'b, a -> a'. This would require two differently named functions in a qualified type system.

Public

What might be useful is allowing for a signature like 'a, b -> c' and associate substitutions with predicate conditions. If 'a' is a vector and 'b' is a matrix then substitute 'b' for 'c'. If 'a' is a matrix and 'b' is a vector, substitute 'a' for 'c'.

Public

Okay so this strategy seems to be working! Hopefully will have some cool code to share soon.

Public

Here's a simple example program:

(top-level ((in int x))
(let ((f (lambda (x) (+ x 1))))
(f 2)))

The only type declaration is on the shader input attribute 'x'. Here's the typed program that the compiler generates:

(t ((primitive int))
(top-level
((in int V0)
(function
V2
(t ((for-all
((tvar T3) (tvar T7))
(-> ((tvar T3)) ((tvar T7)))
(and (= (tvar T3) (primitive int))
(substitute (tvar T7) (tvar T3)))))
(lambda (V1)
(t ((tvar T7))
(primcall
+
(t ((tvar T3)) V1)
(t ((primitive int)) 1)))))))
(t ((primitive int))
(call (t ((-> ((primitive int)) ((primitive int)))) V2)
(t ((primitive int)) 2)))))

The interesting bit is the 'for-all' expression that generalizes the function 'f' to accept any arguments as long as they satisfy the predicate (the 'and' expression). In this case the only valid type for 'x' is an integer.

The compiler is currently unable to emit GLSL code for the function because it doesn't yet know how to translate a 'for-all' into a series of functions for each valid type combination. That's the next step.

Public

Okay now I can emit GLSL for functions with multiple signatures!

Contrived input program:

(let* ((f (lambda (x y) (+ x y)))
(z (f 1.0 2.0)))
(f 3 4))

'z' is a useless binding that just demonstrates that 'f' can be called with floats and ints. good thing I don't do dead code elimination yet.

GLSL output:

void V2(in int V0, in int V1, out int V4) {
int V5 = V0 + V1;
V4 = V5;
}
void V2(in float V0, in float V1, out float V6) {
float V7 = V0 + V1;
V6 = V7;
}
void main() {
float V8 = 1.0;
float V9 = 2.0;
float V10;
V2(V8, V9, V10);
float V3 = V10;
int V11 = 3;
int V12 = 4;
int V13;
V2(V11, V12, V13);
int V14 = V13;
}

Public

I now have basic struct types working. The qualified type logic was extended to support them and overloaded function generation was extended to deal with structs as arguments. I also added function signatures for a number of built-in GLSL functions. I can once again compile the simple vertex shader I use for 2d sprites:

(top-level
((in vec2 position)
(in vec2 texcoords)
(out vec2 frag-tex)
(uniform mat4 mvp))
(outputs
(vertex:position (* mvp
(vec4 (-> position x)
(-> position y)
0.0 1.0)))
(frag-tex texcoords)))

GLSL output:

in vec2 V0;
in vec2 V1;
out vec2 V2;
uniform mat4 V3;
void main() {
float V4 = V0.x;
float V5 = V0.y;
float V6 = 0.0;
float V7 = 1.0;
vec4 V8 = vec4(V4, V5, V6, V7);
vec4 V9 = V3 * V8;
gl_Position = V9;
V2 = V1;
}

Public

I took a look at the type inference code in guile-prescheme and I'm impressed with how elegant it is. However, it's an imperative implementation. My algorithm is functional and I'd like to keep it that way. Once I have something that produces correct output for all of my existing shaders I will try to refactor the code into something nicer.

Public

The next big step was hacking this compiler into my existing shader code so I could actually use the shaders and test that they really work. This required a linking pass that does a bunch of renaming on the separately compiled vertex and shader stages to ensure that vertex output names match up with fragment input names (GLSL needs the names to be the same, ugh!) and that uniform variables between both stages use unique names because they share a global namespace.

Public

ahhh I got my single sprite and batch sprite shaders ported over and *they work*!!!

Public

here's a more complicated shader that I ported to seagull. I use this for stretchable dialog boxes. gist.github.com/davexunit/5d9c

GistSeagull shader with helper functionSeagull shader with helper function. GitHub Gist: instantly share code, notes, and snippets.
Public

My most complicated shader port yet: the stroke and fill shaders for my vector graphics renderer. gist.github.com/davexunit/83e1

Gistvector path shadersvector path shaders. GitHub Gist: instantly share code, notes, and snippets.
Public

Seagull is feeling pleasantly Schemey! There are two big missing features in the language: Loop syntax (GLSL *severely* restricts looping and recursion is *not* allowed so a loop form is essential, unfortunately) and user defined structs. I already have define-shader-type syntax that I've been using with my existing GLSL shader code so I just need to a way to import those into Seagull code and have the compiler generate the GLSL struct declaration.

Public

Both of those language features will be necessary to port my most complicated shaders, which are the phong and physically-based lighting shaders.

Public

okay so I'm going to add syntax for the *very* restricted loops that GLSL allows. GLSL is limited to 'for' loops where the loop variable is initialized to a constant value, incremented by a constant value, compared to a constant value, and actually terminates (you can't say `i = 0; i < 5, i--`.) I intend to not emit 'for' loops at all, opting instead to unroll the loops in a compiler pass. for this to work I'll need a teeny interpreter that figures out all the values of the iterator variable.

Public

since Seagull is purely functional, the loop syntax needs to support a way to accumulate a result. the Schemiest syntax I've come up with is a riff on Scheme's 'do':

(for ((i 0) (< i 5) (+ i 1)) ; iterator
((x 0)) ; accumulators
(+ x i)) ; body

so I implemented this and used the exact code above to test. when I finally got the bugs out I didn't see an unrolled loop at all because the partial evaluator simplified the whole thing to just '10' lol

Public

oops turns out only GL ES has these strict loop limitations. regular GL is a lot more flexible, including being able to express non-terminating loops! didn't read the spec close enough. not sure if I want to do loop unrolling at all, now.

Public

Not much progress on loops but I did do some significant refactors that make it a lot easier to add new primitives and added some REPL commands to make running the compiler more convenient while developing.

Unlisted public

@dthompson
When I hear things like this I always think of Andy Wingo saying "Damnit GCC" while showing benchmarks of different versions of guile.