My best month in years
What causes motivation? Damned if I know! 06 Jan 2021I’ve just had the most productive month since I was laid off in April, and the most productive month without pay in years. Why?
It’s hard to say exactly, but a number of factors came together.
First and foremost, moral support by Maksim Volkau was invaluable. Thank you so much! Sometimes people complain about how users of open-source projects can be demanding and not contribute anything back, but I think that demanding users are not nearly as bad as having no users at all. I make open source software for one reason: I want to make the world a better place. Simple as that. The fact that I have no users and no participants is therefore a massive hole in my life. Jonathan van der Cruysse used to be a heavy user, but his work tapered off a couple of years back for reasons unknown to me, so it was a big boost to have Maksim come in with some bugs and feature requests. On very, VERY rare occasions I get nice message from a lurker, but only a bug report can prove someone’s actually using my software. So, to those rare individuals who step up with a bug report, thank you so much.
Another factor is that I quit looking for a job. Suffice it to say that I am giving up a bunch of EI income in order to do this.
The third factor was that I wanted to make an Blazor version of LeMP, so users could try it online, but first I wanted a macro for building other macros, which the online version would definitely need. For this macro to be as powerful as I wanted it to be, I had to add some new features to LeMP, which was more work than expected - I ended up making a set of related general-purpose classes with names like CollectionWithChangeEvents
, because I wanted to use just one of those classes for an internal design I thought I wanted to use in LeMP - I decided to also write very similar classes (ListWithChangeEvents
and DictionaryWithChangeEvents
) for completeness. But ultimately I didn’t need these classes in the final design, so basically I wasted my time. Still, the classes I built and unit-tested will no doubt be useful for someone, someday. I hope. And then of course I added a macro
macro - there was already a define
macro for doing simple syntactic transforms, but macro
gives you the full power of the C# language. It’s been possible since the beginning to define your own macros, but other than Jonathan I don’t know of anyone who bothered.
Old And Busted*
(* not actually busted)
New Hotness: complex compile-time logic
macro change_temporarily($lhs = $rhs)
{
using Loyc.Ecs;
var tempOld = LNode.Id("old_" + context.IncrementTempCounter());
matchCode(lhs) {
case $prop.$member:
// Avoid evaluating a property or function call twice in the output.
// Also support `ref` attribute: it causes the prop var to be a ref var.
var @ref = LNode.List().AddIfNotNull(node.AttrNamed(EcsCodeSymbols.Ref));
unless (@ref.IsEmpty && prop.IsId && prop.Name.Name.ToLower() == prop.Name.Name)
{
var tempProp = LNode.Id(EcsValidators.KeyNameComponentOf(prop).Name
+ context.IncrementTempCounter());
var propRef = quote([$(..@ref)] $prop);
return quote {
[$(..@ref)] var $tempProp = $propRef;
var $tempOld = $tempProp.$member;
$tempProp.$member = $rhs;
on_finally { $tempProp.$member = $tempOld; }
};
}
}
// In simple cases, use the old logic
return quote {
var $tempOld = $lhs;
$lhs = $rhs;
on_finally { $lhs = $tempOld; }
};
}
void Example() {
ref change_temporarily(GetParent().Mode = Mode.Awesome);
AwesomeModeIsTemporarilyActive();
}
// Generated code (LeMP v2.9.0.1)
void Example() {
ref var GetParent12 = ref GetParent();
var old_11 = GetParent12.Mode;
GetParent12.Mode = Mode.Awesome;
try {
AwesomeModeIsTemporarilyActive();
} finally {
GetParent12.Mode = old_11;
}
}
I also wanted to release semantic version 29 before the web version of LeMP, and to to that I had to finish any breaking changes I wanted to do for version 29, and one thing led to another…
- I had to finish custom literal handling for LES2 and LES3, and this called for a new feature in the language-agnostic Token structure, something I later called “uninterpreted literals” (which means “a literal represented by a type marker and a string without escape sequences that has not been parsed into its actual data type, unless that data type happens to be a string, of course”).
- On a whim, I wanted to investigate how to support non-ASCII identifiers and operators which caused me to build an awesome page that displays every unicode character in existence. Emojis as identifiers? I’m thinking yes.
- To make macros play better with token lists, I added an ability for LES3 token lists to be interrupted and resumed within a single argument list](https://github.com/qwertie/ecsharp/commit/15f0baae7ac7c8846596f891105e0e3279d6a478).
- I stripped out support for
NodeStyle.OneLiner
because that whole feature was a premature optimization I shouldn’t have done in the first place. - Tweaked collection interfaces (again) and other interfaces
- Added
LNodeRangeMapper
so thatcompileTime
andmacro
can figure out, given an error location in the C# output, the location of the error in the original Enhanced C# code. - Added
PreviousSiblings
andAncestorsAndPreviousSiblings
toIMacroContext
so thatmacro
can scan above the macro definition forusing
statements - I wanted to figure out the best way to write printers. Step one: post musings on an old thread. Step two: I noticed I had implemented two separate printer-helper classes and decided to combine their features in one: goodbye
DefaultNodePrinterWriter
andPrinterState
, helloLNodePrinterHelper
! Step 3: incorporate it into all three existing printers. Step 4: deleteDefaultNodePrinterWriter
. - For fun I added an
--eval
command-line option - Laboriously convinced C# to understand
.TryGet(i)
on a type-by-type basis - Added
#preprocessArgsOf
,#preprocessChild
macros to help with those pesky macro order-of-operation issues.
Over at Future of Coding slack, I wanted to post “two-minute week” for the past month, so I started thinking of demos to show. And I thought I wanted a demo that used a switch
expression. Problem: EC# doesn’t support switch expressions yet. Solution: implement those too. Problem: C# 8 switch (and is
) patterns are poorly documented, hideously ambiguous, incompatible with LL(k), and, dare I say, worse than my older Enhanced C# design in some ways. Solution: spend 2 full days on it.
Of course, Enhanced C# is a liberalization of C#, so the obvious question was which features of C# 9 patterns should be included throughout EC#. I decided against adding unary relational operators (like < 0
) everywhere, but in favor of adding the when
operator everywhere, as well as a variant of the switch
operator that is not followed by braces.
And then I thought “why stop there?” and added a where
operator too, because I think some people (e.g. me) would be interested in writing Haskell-style code where you write your code top-down instead of bottom-up. Take, for instance, the code in compileTime
for error handling when you give it some invalid code. What I wanted to write was some code to print an error message:
context.Sink.Error(errorLocation, "{0} - in «{1}»"
.Localized(e.Message, lineOfCodeFromOutput));
But first, I need to figure out the errorLocation
and a lineOfCodeFromOutput
to include in the error message. At this point programmers usually insert code above this point to retrieve the desired information. I’m thinking, no, that harms code readability! Instead what we should do is write the code in a more natural top-down order:
// (I'd prefer the shorter syntax $errorLocation, but
// `var` is what I think the Roslyn team would choose)
context.Sink.Error(var errorLocation, "{0} - in «{1}»"
.Localized(e.Message, var lineOfCodeFromOutput)) where
{
Microsoft.CodeAnalysis.Text.TextSpan range = e.Diagnostics[0].Location.SourceSpan;
var C#Location = new IndexRange(range.Start, range.Length);
var errorLocation = outputLocationMapper.FindRelatedNodes(C#Location, 10)
.FirstOrDefault(p => !p.A.Range.Source.Text.IsEmpty);
lineOfCodeFromOutput = codeText.Substring(var lineStart, (var lineEnd) - lineStart) where {
int column = e.Diagnostics[0].Location
.GetLineSpan().StartLinePosition.Character;
lineStart = range.Start - column;
lineEnd = codeText.IndexOf('\n', lineStart);
if (lineEnd < lineStart)
lineEnd = codeText.Length;
string line = codeText.Substring(lineStart, lineEnd - lineStart);
}
}
The new release won’t actually include the macro necessary to use this feature, but it’s a macro that you could theoretically write yourself. You don’t even need the new version, as Enhanced C# already has a “user-defined operator” feature - you just have to write expr `where` {...}
instead of the slightly better-looking expr where {...}
.
Anyway, while working on C# 8/9 pattern matching I realized that if I didn’t want to write a bunch of ugly custom parser code again, I would need some kind of new feature in my parser generator to give me better control over code generation for recognizers. So I spent two days adding new LLLPG operators called recognizer
and nonrecognizer
to help me out.
And that’s where we are today. In the coming days I’ll be producing videos for Future of Coding, figuring out how to write printers more effectively by implementing a printer for TypeScript, and releasing semantic version 29 of Enhanced C#, LeMP, LLLPG and the Loyc .NET Libraries.
Did I somehow regain my hope — hope that more work would lead to renewed interest in my software and my ideas? Maybe. Hope is a fragile thing. [Edit: on second thought, it wasn’t fragile when I started the EC# project 8 years ago; time has a way of wearing me down.] I can feel the despair clawing at my psyche from below. A couple of times in the past month, I remembered the bad times, like when I would write a ten-page article on a new feature, post it to Reddit and get two upvotes and zero traction. So far I’ve managed to refocus my mind on more positive thoughts. I worry that my success won’t last much longer, but I am confident that I will at least manage to post the videos, release v29 and continue work on the web version of LeMP.
Comments