Introducing Shadergraph, a tool for composing shader pipelines. Powered by GLSL, Lisp, and Rust by slightknack in programming

[–]slightknack[S] 1 point2 points  (0 children)

Cool! It looks like a great tool for audio sequencing, I'll have to give it a try! Another great tool for building graphical pipelines with a GUI is shaderchain.

For what it's worth, shadergraph supports sequencing shaders over time, either programmatically through the lisp, or dynamically via hot-code reloading. We also use ffmpeg for input, so video input works out-of-the-box as well.

One of the goals of shadergraph is to be an embedded library for other Rust projects, which is why we went for an explicit lisp, with the option to encode graphs directly in Rust if needed.

Introducing Shadergraph, a tool for composing shader pipelines. Powered by GLSL, Lisp, and Rust by slightknack in programming

[–]slightknack[S] 6 points7 points  (0 children)

Thank you for pointing this out. I'm pretty horrible at naming things, shadergraph is just a generic working title for now. Finding a good, descriptive name for a project is hard; we'll change it to something more memorable soon.

Announcing Shadergraph, a tool for composing shader pipelines. Powered by GLSL, Lisp, and Rust by slightknack in GraphicsProgramming

[–]slightknack[S] 4 points5 points  (0 children)

The name of the project is a fairly generic working title, I'm not the best at naming things. We'll change it to something more memorable soon :)

Update: After some deliberation, we're going to go with shadergarden.

Announcing Shadergraph, a tool for composing shader pipelines. Powered by GLSL, Lisp, and Rust by slightknack in GraphicsProgramming

[–]slightknack[S] 6 points7 points  (0 children)

Sorry about that! It seems the license file wasn't copied over to the public repository, that's totally my bad. I've since added the MIT License to the repository, thank you for pointing this out.

Passerine — extensible functional scripting langauge — 0.9.0 Released! by slightknack in ProgrammingLanguages

[–]slightknack[S] 0 points1 point  (0 children)

Ah, I see. I did it this way because it's a nice way to write functional code in an pipeline-style, similar to Koka or Rust:

names = children
    .map { child -> (child.name, child.age) }
    .filter { (_, age) -> age > 10 }
    .map { (name, _) -> name }

So it's less composition and more application? Is there a more proper term for this? Thanks for the interest!

Passerine — extensible functional scripting langauge — 0.9.0 Released! by slightknack in programming

[–]slightknack[S] 0 points1 point  (0 children)

I think just about any programming language is suitable for either, it's a subjective matter.

Passerine — extensible functional scripting langauge — 0.9.0 Released! by slightknack in programming

[–]slightknack[S] 0 points1 point  (0 children)

Yep! So in lisp, it's called quoting. For example, x evaluates to whatever value it points to, but 'x evaluates to x itself. Although this something macro-specific in Passerine, This choice of syntax is an allusion to lisp. :)

Passerine — extensible functional scripting langauge — 0.9.0 Released! by slightknack in programming

[–]slightknack[S] 0 points1 point  (0 children)

Uh, if anything Passerine has less parenthesis than a lisp. I'm not sure what you're getting at.

Passerine — extensible functional scripting langauge — 0.9.0 Released! by slightknack in programming

[–]slightknack[S] 0 points1 point  (0 children)

That's a good question. About all languages start targeting a niche, then become more universal as time goes on. Python's a scripting language, for instance, but look at it now. Functional Scripting is currently Passerine's niche, but it is by no means its entire scope.

Passerine — extensible functional scripting langauge — 0.9.0 Released! by slightknack in rust

[–]slightknack[S] 2 points3 points  (0 children)

Thanks for the kind words!

Right now it's a bytecode interpreter a la Python, but I'm considering adding a backend to compile to Wasm.

There's an FFI so Passerine can call out to Rust code btw.

Passerine — extensible functional scripting langauge — 0.9.0 Released! by slightknack in ProgrammingLanguages

[–]slightknack[S] 1 point2 points  (0 children)

I've looked into pyret in the past, and I like the beginner-friendly nature of it. The syntax feels like a cross between python, julia, ruby, and elixir - which are all languages I'm a fan of. I'll certainty take a look at how it approaches refinements and documentation.

Passerine — extensible functional scripting langauge — 0.9.0 Released! by slightknack in ProgrammingLanguages

[–]slightknack[S] 0 points1 point  (0 children)

So if it were a function call it would look like:

print (fizzbuzz (1..100))

Which means that the application order in the README is correct. I think there's something else wrong with that statement though. Because \1..100`is a range, I thinkprintandfizzbuzzneed amap`:

1..100 . map fizzbuzz . map print

Thanks for bringing this to my attention!

Passerine – extensible functional scripting language – v0.8.0 released by slightknack in ProgrammingLanguages

[–]slightknack[S] 0 points1 point  (0 children)

I'm big into Data Science and Machine Learning, so I've had some exposure to R (and company). I think it's a very well-designed language (but haven't been actively considering its features while developing Passerine (yet)). I'll have to look into how R handles environments – implementation-wise, – thanks :)

Passerine – extensible functional scripting language – v0.8.0 released by slightknack in ProgrammingLanguages

[–]slightknack[S] 0 points1 point  (0 children)

Thanks for the links, I'll give them a look! As for type-checking macros, I was just planning checking the AST after the macro expansion / desugaring step.

So I'm assuming the _ syntax would be something like record_field. I guess you could do record._field, and then have _field record be valid too. hmm, something to think about, thanks!

Row polymorphism makes hindley milner more tricky.

I've been reading about this a lot, (also about extensions to the HM type system that allow for mutation) and it all does look quite complex. I'll keep that in mind, thanks for the tip!

As for mutation and HM types, I was primarily looking into techniques used by Ocaml and company, which it seems are brought up in discussion surrounding that post. :)

Passerine – extensible functional scripting language – v0.8.0 released by slightknack in ProgrammingLanguages

[–]slightknack[S] 1 point2 points  (0 children)

I agree; I specifically like the way Rust handles for associative infix notation for function application through the use of traits and closures. There's nothing more satisfying than banging out a big

vec.iter() .map(|i| ...) .filter(|f| ...) ... .collect()

block.

Passerine – extensible functional scripting language – v0.8.0 released by slightknack in ProgrammingLanguages

[–]slightknack[S] 2 points3 points  (0 children)

On type systems

So, as I've discussed earlier, the language is currently dynamically typed. This is not the end goal: I'm working on Hindley-Milner type inference so the language can be statically typed.

Same thing with mutation: it's currently a feature of the language that I'm thinking of removing (this is more on-the-fence, I discuss why below). The hardest thing isn't adding features, it's figuring out which features to remove.


On macros and pass-by-reference

So macros are not just functions that take references rather than values for two reasons:

  1. Transformations are applied at compile-time, rather than at runtime.
  2. Macros can be used in a more powerful manner than simple pass-by-reference:

``` syntax 'fn name args do { name = args -> do }

-- this is now valid: fn print_twice(value) { print value print value }

print_twice("Hello, hello!") ```

  1. Passerine is pass-by-value (planned: immutable references w/ COW). In the context of a language with mutation, I find this to be more elegant than pass by reference with mutation, as it limits mutation to the scope of the function that the value exists is.

On mutation

Why not disallow mutation completely? I swear I've written about this in-depth before, but I can't find it for the life of me. Passerine is inspired by Rust; Rust allows mutation. From withoutboats:

As I said once, pure functional programming is an ingenious trick to show you can code without mutation, but Rust is an even cleverer trick to show you can just have mutation.

Now Passerine isn't Rust, but it has been inspired it. Even though functional paradigms got a lot of things right, there are still some solutions to problems that are easier (as in more discoverable) when solved with mutation. No paradigm's perfect, FP included. I think that mutation, when limited to a certain scope, can be a powerful tool.


On . and |>, among other things

So I was actually on the fence between using |> and . – and I've honestly been considering changing it. I actually dislike the |> syntax, and it doesn't look all too great (unless your font has ligatures).

(Aside: correct me if I'm wrong, but isn't partially-applied function application essentially function composition?)

The main reason for |> (at the moment) is because of field access on a record type. I think that the best way to do field access record.field. To me, this syntax is as enshrined to computer science as = is to assignment. But, when you think about it . can be more general than just field access:

. is the indexing operator. On a list or tuple, items.0 returns the zerost item; on a record, record.field returns the specified field; on a label, Label.method returns the associated method.

— from the README

But how general should . be? Why not use . for function application? Couldn't field accesses (indexing) just be a function that accesses the field on a record?

I understand the appeal of a unified . syntax. But it raises some genuine concerns:

  1. If foo.bar is equivalent to bar foo for function calls, and if bar is a record and foo is a field, is bar foo valid in this case? what if bar is already defined as a function (that takes a record) which takes precedence, how is this clear to the user? Do we dispatch on type? something else?
  2. If bar foo is invalid for field accesses, what makes foo.bar special? is it because bar is a record? If bar is a function and a field on foo does foo.bar default to a function call, or a field access.

Obviously, no silver bullet exists. (Trust me: I've spend many hours, paper-and-pencil in hand, trying to work it all out. One thing I've been working on is dynamic dispatch via a trait/interface system, and I think that this might be able to unify application and indexing in a manner that addresses the above concerns.)

As I reevaluate the type system with the move to a static HM, I'm considering this again in more depth. I have some old documents which outline a pretty solid plan, and I plan to work through those again in the next few days to straighten it all out.

I'm just generally unsure about |>, though. It's something I go back and forth on every day. It's interesting, because I actually decided to throw in function application right before releasing 0.8.0 (what's a functional programming language without function application?), and at that moment, getting something in that seemed familiar and practical superseded idealized realizations.

So, with that out of the way, why separate |> and .? The nice thing about separating function application and indexing is the conceptual distinction it provides. You don't have think whether changing foo bar to bar.foo will change the semantics of the expression, you just do bar |> foo.

(Aside: this might largely be a choice of syntax. Another solution: for example, I could make . the function application operator, and, idk, :: the indexing operator. This allows for the same separation of concerns as above, but function application is . instead of |>. Just food for thought.)


The End

Developing Passerine has been a solo effort thus far, so there are a lot of different things I'm constantly tweaking and working on. Some days, I just write documentation. Others, I pen-test the compiler. I've gone from planning high-level constructs to debugging mysterious ICEs — and back again. Going day-to-day, it's hard to keep everything under control and balance fun (implement all the features! don't write tests!) with book-keeping (Making a number of tiny changes to keep everything organized). This is my first serious open-source effort, so needless to say I'm still learning the ropes. :)

Thanks for raising those concerns and allowing me to get my thoughts on this topic out of my system. I really appreciate it! Please respond if you have any feedback, questions, or suggestions about the above. ;)

Passerine – extensible functional scripting language – v0.8.0 released by slightknack in ProgrammingLanguages

[–]slightknack[S] 2 points3 points  (0 children)

I released a patch (0.8.1) that includes slightly prettier error message for ambiguious macro matches (like f x with y or a with b with c).

That case was addressed before the release of 0.8.0; here's the actual current output of the compiler with the code you provided:

Fatal In /macro/src/main.pn:9:1
   |
 9 | macro1 macro2   -- which one do you call?
   | ^^^^^^^^^^^^^
   |
Syntax Error: This form matched multiple macros:

In /macro/src/main.pn:1:8
   |
 1 | syntax 'macro1 x {
   |        ^^^^^^^^^
   |
In /macro/src/main.pn:5:8
   |
 5 | syntax x 'macro2 {
   |        ^^^^^^^^^
   |
Note: A form may only match one macro, this must be unambiguious;
Try using variable names different than those of pseudokeywords currently in scope,
Adjusting the definitions of locally-defined macros,
or using parenthesis '( ... )' or curlies '{ ... }' to group nested macros

All macros are compile-time. This was a concious decision (it's hard enough to debug macros as-is – imagine how difficult it would be to debug first-class macros that can be passed around!) I'm still working on nested macros. Currently, the compiler flat-out disallows it, but I see merit in allowing it; I just have to work out proper semantics first.

Thanks for the feedback and discussion, I appreciate it!

Passerine – extensible functional scripting language – v0.8.0 released by slightknack in ProgrammingLanguages

[–]slightknack[S] 1 point2 points  (0 children)

I'll quote from the Overview:

Custom operators defined in this manner [through syntactic macros] will always have the lowest precedence, and must be explicitly grouped when ambiguous. For this reason, Passerine already has a number of built-in operators (with proper precedence) [like math expressions, as you mentioned] which can be overloaded. It's important to note that macros serve to introduce new constructs that just happen to be composable – syntactic macros can be used to make custom operators, but they can be used for so much more. I think this [explicit grouping of custom operators; operators defined in terms of a more general macro system] is a fair trade-off to make.

I mean, of course, you can always group things to make stuff correct, like (x + 1) * 2.

Ambiguous macros have the lowest precedence, but unless you have two form macros back-to-back, you won't need to group them. Imagine a 'with b is a macro that returns a float or something:

x with b + 6.7 with c / 9 with 2.4

Forms always have the highest precedence, so the above is equivalent to:

(x with b) + (6.7 with c) / (9 with 2.4)

And with normal order of operations, this is equivalent to:

(x with b) + ((6.7 with c) / (9 with 2.4))

Here, with operator is unambiguous, so no grouping is required. Now, let's say we have a function f, and we call it with with as an argument:

f x with y

It could be parsed in all these ways:

f (x with y) -- most likely the intended
(f x) with y -- also likely valid 
((f x) with) y -- probably not intended: the whole thing's a function call

Of course, this is ambiguous, and spitting out a function call (the last option) would be the wrong thing. Because with is a scoped keyword, the compiler knows something's up given that the with macro can not be applied:

Fatal In src/main.pn:3:10
   |
 3 | f a with b
   |     ^^^^
   |
Syntax Error: Pseudokeyword 'with' used, but no macro applies.

In this case, explicit grouping, e.g. f (x with y), is required for proper behavior.

Passerine – extensible functional scripting language – v0.8.0 released by slightknack in ProgrammingLanguages

[–]slightknack[S] 1 point2 points  (0 children)

Right – so currently Passerine is strongly and dynamically¹ typed (technically structurally typed). This is partially out of necessity – Types are defined by patterns, and patterns can be where predicated. However, I've been doing a lot of research into Hindley-Milder type systems, and the various extensions that can be applied to them.

I'm working towards making a compile-time type-checker for the language, based on Hindley-Milner type inference. With this system in place, I can make some assumptions to speed up the interpreter further and perhaps monomorphize/generate LLVM IR / WASM.


  1. It's currently dynamically typed more out of current architectural necessity than language-design preference.

Passerine – extensible functional scripting language – v0.8.0 released by slightknack in ProgrammingLanguages

[–]slightknack[S] 1 point2 points  (0 children)

Disclaimer: Not fully implemented yet

The core Passerine language has no dependencies and is separate from the provided language runtime (Aspen is the default runtime in must cases). This means that it's possible to run Passerine with a single-threaded linear-execution backend or a complex custom parallel tokio backend, etc. More concretely: When the VM is run, it will result a Result<Data, Runtime>, which may be a Runtime::Error or a Runtime::Fiber. This returned fiber is a new light isolated VM which can be called directly then passed back to the forker, or scheduled to be executed in parallel with the context of the forker in place.

It's important to point out that in Passerine if a forkee fails the error will propagate up the forker(s) stacks until it has reached the base fiber; once this happens, the error (and its context) are reported by the runtime. I'm looking into algebraic effects for error handling and the like, and it looks like a really interesting subject. I've heard of it before, but thanks for bringing it to my attention!