Skip to content

Instantly share code, notes, and snippets.

@dfl
Last active January 27, 2026 17:19
Show Gist options
  • Select an option

  • Save dfl/45e894d28e8df238adc6b6109a92d827 to your computer and use it in GitHub Desktop.

Select an option

Save dfl/45e894d28e8df238adc6b6109a92d827 to your computer and use it in GitHub Desktop.
Faust PR proposals for review

Add default parameter values for function definitions

Summary

This PR adds support for default parameter values in function definitions, reducing boilerplate and improving API ergonomics:

// Define with defaults
lowpass(freq, q=0.707) = fi.resonlp(freq, q, 1);

// Call - omit defaulted parameters
process = lowpass(1000);        // q = 0.707
process = lowpass(1000, 1.5);   // q = 1.5

Motivation

Currently, Faust functions require all parameters at every call site, even when sensible defaults exist:

// Without defaults - must specify q every time
lowpass(freq, q) = fi.resonlp(freq, q, 1);
process = lowpass(1000, 0.707), lowpass(2000, 0.707), lowpass(500, 0.707);

// With defaults - cleaner code
lowpass(freq, q=0.707) = fi.resonlp(freq, q, 1);
process = lowpass(1000), lowpass(2000), lowpass(500);

This could be especially valuable for library functions where most users want standard values but experts need tunability.

Syntax

// Single default
gain(x, amount=1.0) = x * amount;

// Multiple defaults (must be rightmost parameters)
mix(a, b, ratio=0.5) = a * (1-ratio) + b * ratio;
env(attack=0.01, decay=0.1, sustain=0.7, release=0.3) = ...;

// Defaults can be expressions
SR = 48000;
nyquist(sr=SR/2) = sr;

// Defaults can reference earlier parameters
scale(x, factor, offset=factor*0.1) = x * factor + offset;

Constraints:

  • Parameters with defaults must come after parameters without defaults
  • Default expressions are evaluated at definition time

Implementation

  • Parser: Extended function parameter syntax to accept = expression
  • Boxes: Default values stored by position (nil for required params)
  • Eval: Modified function application to fill missing arguments from defaults

Testing

Pass tests:

  • default-params-basic.dsp - single default parameter
  • default-params-multiple.dsp - multiple defaults in one function
  • default-params-expression.dsp - defaults using expressions/constants

Note on diff size

The large diff in generated files (faustparser.cpp, etc.) is from flex/bison regeneration. Actual source changes:

File Lines changed
faustparser.y +97
global.hh +6
sourcereader.cpp +16/-9

Backward Compatibility

Fully backward compatible - existing function definitions without defaults work unchanged.

Future Work

Combines naturally with keyword arguments (separate PR):

filter(freq, q=0.707, gain=1.0) = ...;

// All these work:
process = filter(1000);
process = filter(1000, 1.5);
process = filter(1000, gain: 2.0);        // skip q, override gain
process = filter(freq: 1000, gain: 2.0);  // named + default

Branch

Add -il option to inline all libraries

Discussion: grame-cncm/faust#832

Summary

This PR adds a new -il (--inline-libraries) option that produces a self-contained DSP file with all library code inlined:

faust -il myfile.dsp -o myfile_standalone.dsp

The output preserves readable Faust source code, unlike -e which exports an unreadable expanded box tree. Tree-shaking automatically removes unused definitions to keep output minimal.

Motivation

From issue #832: Users need self-contained DSP files for sharing, archiving, or using in environments without library access. The existing -e option exports expanded DSP but produces unreadable output.

Example

Input (mytest.dsp):

import("mymath.lib");
fx = library("myeffects.lib");
process = _ : half : fx.gain;

mymath.lib:

half(x) = x * 0.5;
double(x) = x * 2;  // unused by this program

Output with -il: (note: double is removed by tree-shaking)

// Self-contained DSP file generated with -il option
// Inlined libraries: 2
// Tree-shaking: enabled (5 definitions used)

// ===== Imported: mymath.lib =====
half(x) = x * 0.5;
// ===== End of: mymath.lib =====

// Library: mymath.lib
__lib_mymath = environment {
half(x) = x * 0.5;
};

// Library: myeffects.lib
__lib_myeffects = environment {
gain = *(0.8);
};

// ===== Main DSP =====
fx = __lib_myeffects;
process = _ : half : fx.gain;

How It Works

  1. Evaluation phase: Runs normal Faust evaluation, tracking which definitions are used
  2. Collection phase: Recursively discovers all import() and library() dependencies
  3. Output phase:
    • import() content inlined directly at top level (into global scope)
    • library() content wrapped in environment { } as top-level definitions
    • Tree-shaking: Only used definitions are included in output
    • Files used as both import() and library() are handled correctly
    • Circular dependencies detected and reported
    • Duplicate variable definitions skipped

Tree-Shaking

The -il option automatically removes unused definitions by:

  1. Evaluating the program first to determine which symbols are actually used
  2. Tracking all symbol lookups during evaluation in gUsedDefinitions
  3. Filtering the output to only include definitions whose names appear in the tracked set

This significantly reduces output size when importing large libraries but only using a few functions.

Implementation

As suggested in the original discussion, this reuses infrastructure from the -i option:

  • Uses gGlobal->gAlreadyIncluded set for tracking visited files
  • Uses fopenSearch() for resolving library paths
  • Similar recursive inject pattern

Why new code was needed on top of -i:

The -i option handles C++ #include in architecture files with simple text substitution. The -il option needs Faust-specific semantics:

-i (architecture) -il (libraries)
Detects #include <faust/...> Must parse import("...") and library("...")
Just copies file content import() → inline directly
library() → wrap in environment { }
Single use pattern Files can be used as both import AND library
Must track variable definitions to avoid duplicates
Tree-shaking removes unused definitions
File Changes
global.hh +2 lines (flags)
global.cpp +8 lines (option parsing, init, help)
libcode.cpp +15 lines (integration)
sourcereader.hh +3 lines (declaration)
sourcereader.cpp +280 lines (inlining + tree-shaking logic)
eval.cpp +8 lines (symbol tracking)

Testing

# Simple test
faust -il test.dsp -o test_inlined.dsp
faust -e test_inlined.dsp  # Verify it compiles

# Tree-shaking in action
faust -il uses_one_function.dsp -o small_output.dsp

Option Naming

Follows the pattern of -it (--inline-table):

  • -il = inline libraries
  • -it = inline table

Backward Compatibility

Fully backward compatible - new option, no changes to existing behavior.

Branch

Add keyword arguments for UI primitives and user-defined functions

Summary

This PR introduces Ruby/Python-style keyword arguments to Faust, enabling more readable and self-documenting code for both UI primitives and user-defined functions:

// UI primitives
hslider("volume", init: 0.5, min: 0.0, max: 1.0, step: 0.01)

// User-defined functions
mix(a, b, ratio) = a * (1-ratio) + b * ratio;
process = mix(dry, wet, ratio: 0.7);

Motivation

Faust's UI primitives require multiple positional arguments that are easy to confuse:

// Which is init? min? max? step?
hslider("volume", 0.5, 0.0, 1.0, 0.01)

Similarly, user-defined functions with multiple parameters benefit from named arguments for clarity at call sites.

Syntax

UI Primitives

// Before: positional only
hslider("volume", 0.5, 0.0, 1.0, 0.01)

// After: keyword arguments (any order)
hslider("volume", init: 0.5, min: 0.0, max: 1.0, step: 0.01)
hslider("volume", min: 0.0, max: 1.0, init: 0.5, step: 0.01)

Supported: hslider, vslider, nentry, hbargraph, vbargraph

User-Defined Functions

filter(freq, q, gain) = ...;

// Mixed positional + kwargs
process = filter(1000, q: 0.7, gain: 1.0);

// All kwargs (any order)
process = filter(gain: 1.0, freq: 1000, q: 0.7);

Constraints (Ruby-style):

  • Keyword arguments must come after positional arguments
  • All required parameters must be provided
  • Unknown keywords produce clear error messages

Error Messages

error: hslider: unknown keyword 'foo'
Valid keywords are: init, min, max, step

error: hslider: missing required keyword 'step'
Required keywords are: init, min, max, step

error: unknown keyword argument 'c'

Implementation

  • Lexer: Added KWARG token (pattern: {ID}":")
  • Parser: Added kwarg, kwarglist grammar rules
  • Boxes: Added BOXKWARG box type for keyword argument representation
  • Eval: Modified applyList() to resolve kwargs to parameter positions

Testing

Pass tests:

  • kwargs-ui-primitives.dsp - all UI primitive variants
  • kwargs-functions.dsp - user-defined function calls
  • kwargs-mixed.dsp - combining both in one program

Error tests:

  • kwargs-unknown-ui.dsp - unknown keyword for UI primitive
  • kwargs-missing-ui.dsp - missing required keyword
  • kwargs-unknown-func.dsp - unknown keyword for function

Note on diff size

The large diff in faustlexer.cpp, faustparser.cpp, and faustparser.hpp is due to these being generated files. The actual source changes are:

File Lines changed
faustlexer.l +1
faustparser.y +130
boxes.cpp +14
boxes.hh +7
eval.cpp +114
global.cpp +1
global.hh +1

Backward Compatibility

Fully backward compatible - existing positional syntax continues to work unchanged.

Future Work

Integration with default parameters

This feature is designed to integrate with default parameters (separate PR). When rebasing onto default-params:

  1. Extend DefParamMap to store parameter names (currently stores only default values by position)
  2. Update buildBoxApplWithDefaults() to handle mixed positional + kwargs
  3. Resolve kwargs to positions using stored parameter names

After integration:

filter(freq, q=0.707) = ...;
process = filter(freq: 1000);  // q defaults to 0.707

Branch

Add Python-style selective imports

Summary

This PR adds a new import syntax that allows importing specific definitions from a library file, optionally with aliasing:

from "maths.lib" import PI, SR;
from "filters.lib" import lowpass, highpass as hp;

Motivation

Currently, importing a library dumps all its definitions into the namespace, requiring the use of short prefixes like ma., fi., etc:

import("stdfaust.lib");
process = fi.lowpass(2, ma.PI * 100);

This has two drawbacks:

  1. Cryptic prefixes - ma, fi, os etc. are not self-documenting
  2. Namespace pollution - entire libraries are imported even when only one or two definitions are needed

The new syntax allows explicit, selective imports:

from "maths.lib" import PI;
from "filters.lib" import lowpass;
process = lowpass(2, PI * 100);

Syntax

// Import specific names
from "library.lib" import name1, name2, name3;

// Import with aliasing
from "library.lib" import originalName as aliasName;

// Mixed
from "maths.lib" import PI, SR as sampleRate;

The syntax mirrors Python's from X import Y and uses the existing , (PAR) separator that Faust already uses for argument lists.

Implementation

  • Lexer: Added FROM and AS keywords
  • Parser: Added grammar rules for from uqstring import importnames
  • AST: New selectiveImportFile(filename, names) tree type
  • Source reader: Modified expandRec() to filter definitions when expanding selective imports, with optional renaming for aliases

The implementation reuses existing infrastructure - selective imports are expanded during the same phase as regular imports, just with filtering applied.

Testing

Tested with:

  • Basic selective import from a simple library
  • Multiple imports in one statement
  • Aliasing (import X as Y)
  • Importing from standard libraries (maths.lib)

Note on diff size

The large diff in faustlexer.cpp, faustparser.cpp, and faustparser.hpp is due to these being generated files that are regenerated by flex/bison. The actual source changes are minimal:

File Lines changed
faustlexer.l +2
faustparser.y +14
boxes.cpp +9
boxes.hh +3
global.cpp +1
global.hh +1
sourcereader.cpp +40

Backward Compatibility

Fully backward compatible - existing import syntax continues to work unchanged.

Branch

@sletz
Copy link

sletz commented Jan 27, 2026

  • In general, we don't want to add new syntax capabilities if there is no really good reason for that. After discussion with @orlarey this morning, we think that the current syntax is good enough to correctly express Add default parameter values for function definitions and Add keyword arguments for UI primitives and user-defined functions features, like for instance, using environments and explicit-substitution syntax:
import("stdfaust.lib");

// Filter parameters in an environment  
filterParams = environment {
    freq = 1000;
    Q = 0.707;
    gain = 1.0;
};

// Filter parametrized with an environment
myFilter(p) = fi.resonlp(p.freq, p.Q, p.gain);

// Use-cases
//process =  no.noise : myFilter(filterParams);
//process =  no.noise : myFilter(filterParams[freq = 2000;]);
process =  no.noise : myFilter(filterParams[freq = 200; Q = 2.0;]);

Another reason not to go for the proposal is the difficulty to keep a clean and simple semantic with the Faust "abstractions follow a full curryfied model", mixed with the new proposed model.

  • For the Add Python-style selective imports proposal, the same kind of remark: it can be expressed reasonably well with the current syntax, like for instance using: lp = library("filters.lib").lowpass to import and rename a definition from the filters.lib library.

  • For the Add -il option to inline all libraries, it has to be checked how well it scales, so for instance all DSP in examples should be tested. This can be done by adding new targets in this Makefile. If this works well, then we don't see any reason to define a new option, and would better consider your proposal as a improvement of the current -e option.

@dfl
Copy link
Author

dfl commented Jan 27, 2026

@sletz thanks to you both for for taking the time to review!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment