Template Literals in Sc?

Is there something closely related to template literals in JS or f-strings in Python inside SuperCollider?

No. It was proposed a while ago and met with some skepticism (which might not have been warranted). (Brian had proposed it and, unfortunately, I and another list member argued against it, on the grounds of “too many ways to do things.” The objections got a bit [maybe a lot] too strenuous, which I still regret.)

But… during that discussion, I did actually say, “Well, if we’re not sure whether or not it’s a good idea, we could use the preprocessor to prototype it and try it out, see if it’s comfortable.” But that post went ignored. Nobody tested it, nobody commented on it.

Anyway, I saved the prototype, you can give it a shot.

hjh

// this block installs the f"" preprocessor
(
var
convertFStrings = { |code|
	var stream = CollStream(code), out = String.new, ch, closeQuote;
	while {
		ch = stream.next;
		ch.notNil
	} {
		case
		{ ch == $f and: { stream.peek == $" } } {
			stream.next;  // swallow quote
			out = out ++ parseFString.(stream);
		}
		{ ch == $" or: { ch == $' } } {  // scan a whole normal string/symbol literal, with escapes
			closeQuote = ch;
			out = out ++ ch;
			while {
				ch = stream.next;
				ch.notNil and: { ch != closeQuote }
			} {
				if(ch == $\\) {
					out = out ++ ch ++ stream.next;  // escape, esp. for quotes
				} {
					out = out ++ ch;
				}
			};
			if(ch.notNil) { out = out ++ ch };
		}
		{
			out = out ++ ch;
		};
	};
	out;
},
parseFString = { |stream|
	// assumes 'f"' is already scanned; return: code converted to "str".format(...)
	var start = stream.pos, list = List.new, formatStr = String.new, out, ch;
	while {
		ch = stream.next;
		ch.notNil and: { ch != $" }
	} {
		if(ch == ${ and: { stream.peek == ${ }) {
			stream.next;  // swallow second brace
			parseOneExpression.(stream, list);
			formatStr = formatStr ++ "%";
		} {
			formatStr = formatStr ++ ch;
		};
	};
	if(ch.isNil) {
		Error("open-ended f-string: %".format(stream.collection[start .. start + 20])).throw;
	};
	if(list.size >= 1) {  // .format only if there's something to format
		out = CollStream.new;
		out << $" << formatStr << $" << ".format(";
		list.do { |expr, i|
			if(i > 0) { out << ", " };
			out << expr;
		};
		out << ")";
		out.collection
	} {
		// -1 -- we need the opening quote, and don't include .next char
		stream.collection[start - 1.. stream.pos - 1]
	}
},
parseOneExpression = { |stream, list|
	// assumes '{{' is already scanned; result: item added to list
	var start = stream.pos, out = String.new, ch;
	while {
		ch = stream.next;
		ch.notNil and: { ch != $} or: { stream.peek != $} } }
	} {
		// check for nested f-string
		if(ch == $f and: { stream.peek == $" }) {
			stream.next;  // swallow quote
			out = out ++ parseFString.(stream);
		} {
			out = out ++ ch;
		};
	};
	if(ch.isNil) {
		Error("open-ended string interpolation argument: %".format(
			stream.collection[start .. start + 20]
		)).throw;
	};
	list.add(out);
	stream.next;  // scan second brace
	stream
};

// some tests

c = CollStream("abc}}xyz");
l = List.new;
parseOneExpression.(c, l);
l.debug("list, expected [ \"abc\" ]");
c.collection[c.pos..c.pos+5].debug("stream state, expected \"xyz\"");

c = CollStream("abc = {{abc}}\"; xyz");
parseFString.(c).debug("parseFString test");

c = "var abc = 10.rand; f\"abc = {{abc ++ f\"nested string {{xyz}}\"}}\".postln;";
convertFStrings.(c).debug("nested conversion");

c = "\"normal \\\"string\\\" ending with f\".postln";
convertFStrings.(c).debug("escaped \"quotes\" and closing f");

c = "'abcdef\"xyz'";
convertFStrings.(c).debug("weird symbol");

try {
	convertFStrings.("f\"{{abc}xyz\"")
} { |error|
	if(error.errorString.beginsWith("ERROR: open-ended string interpolation argument")) {
		"open-ended string interpolation argument detected OK".debug;
	} {
		error.throw;
	}
};

try {
	convertFStrings.("f\"{{abc}}xyz; 123")
} { |error|
	if(error.errorString.beginsWith("ERROR: open-ended f-string")) {
		"open-ended f-string detected OK".debug;
	} {
		error.throw;
	}
};

thisProcess.interpreter.preProcessor = convertFStrings;
)

// With the preprocessor installed,
// evaluate this block just like any other
(
var abc = 10.rand;
"normal string, OK".postln;
f"no expressions, OK".postln;
f"abc = {{abc}}, abc+1 == {{abc+1}}".postln;
f"abc = {{abc.asString ++ f" plus nested {{abc*2}}"}}".postln;
)
1 Like

thankyou James, super helpful – shall have a play with this shortly!

~format = {  
	|str|
	var bt = Object.getBackTrace.caller;
	var toResolve = Set();
	var resolved = ();
	var find = str.findRegexp("\\$\\{.+?\\}");
	
	find.do {
		|sub|
		toResolve.add(sub[1][2..(sub[1].size-2)].asSymbol);
	};
	
	while { bt.notNil && toResolve.notEmpty } {
		toResolve.do {
			|name|
			var index = bt.functionDef.varNames !? _.indexOf(name);
			if (index.notNil) {
				resolved[name.asSymbol] = bt.vars[index];
			};
		};
		toResolve = toResolve - resolved.keys;
		bt = bt.context;
	};
	resolved.keysValuesDo {
		|name, val|
		str = str.replace("${%}".format(name), val.asString);
	};
	str;
};
{
	var x = 4, y = 6, result;
	result = x * y;
	
	~format.("${x} * ${y} = ${result}").postln;
	
	{
		result = x + y;
		~format.("${x} + ${y} = ${result}").postln;
	}.defer(0.25);
}.();

This will pull the formatting from variables in scope, and doesn’t require preprocessing. The regex replacement isn’t perfect - you’d probably want something like James’ parser, or better just implement it as a primitive…

Note that you should be able to more or less the same thing as an extension on string, so you can e.g. do f("some ${string}").

1 Like

That’s also kinda cool.

getBackTrace sometimes crashes. Not as often as it used to, but I saw that a couple of times lately. I wouldn’t rely on it to do real work.

hjh

very interesting! thankyou both, and @scztt for the alternate option.
I will dabble and relight this thread if I run into any questions :+1:t2: