Compatible by default
Existing HolyC keeps its meaning. New behaviour is switched on for our own code and switched off again before the prompt returns.
HC+
HC+ is our improved HolyC. It keeps the language, the live compiler and the edit-and-run loop, and fixes the handful of things that cost us the most: speed, precedence, a missing keyword, crashes and tooling.
Status: a plan backed by measurements. HC+ is not released; items marked Planned are not built yet.
Existing HolyC keeps its meaning. New behaviour is switched on for our own code and switched off again before the prompt returns.
The compiler rebuilds and hot-loads in about three seconds on the live CD. Editing and running stays instant. No transpiler, no LLVM front end.
Every item below carries a number from the same VM, or says it is an estimate.
The most common source of quiet bugs when moving code between C and HolyC. Drag the sliders.
a*b>>1In HolyC the shift operators bind tighter than multiplication. HC+ switches the compiler to C order around our own code, so code ported from C means what it says.
Blend: naive loop against two pixels a pass with register variables. Float: x87 against SSE2. The last row compares a full OS rebuild with rebuilding only the compiler in the running system; the 150 s figure is a rough comparison, not a measurement.
Twelve changes, filtered by area.
| # | Change | What it does | Effort | Evidence |
|---|---|---|---|---|
| 1 | Hot loops as pointer walks | Write per-pixel loops as pointer walks in small functions, two pixels a pass. The blend loop falls from 11 ns to 0.8 ns a pixel. | S | Measured |
| 2 | SSE2 for floating point | Enable the SSE registers once at start-up and move float kernels to SSE2: 67 ns to 6 ns a multiply-add under emulation. | S | Measured |
| 3 | Test harness over serial | Boot or restore the snapshot, send a test file, read results over the serial port, assert from Python. | S | Prototype exists |
| 4 | C precedence, switched in | The precedence table is runtime data. Sixteen assignments switch the live compiler to C order, sixteen restore it. | S | Verified |
| 5 | continue | Add the missing keyword to the statement parser. Loops already carry a break label. | S | Planned |
| 6 | Catchable faults for the desktop | A page fault in an app becomes an exception the desktop catches, so a bad pointer drops one frame instead of opening the debugger. | M | Planned |
| 7 | Peephole passes | Drop a redundant move, fold address arithmetic, reuse constants: about 1.4x on integer loops. | M | Estimated |
| 8 | Sampling profiler | A timer-driven sampler that reports where frames go, without guessing. | M | Planned |
| 9 | SSE2 in the compiler backend | Float code fast everywhere, without hand assembly: 8 to 11x on F64. | L | Planned |
| 10 | Register arguments for flagged functions | Cheaper calls for callbacks and widgets: about 2x on call-heavy paths. | M | Estimated |
| 11 | Host lint and formatter | A checker for mixed operators, ASCII-only source and unescaped dollar signs, and a token-based indenter. | M | Planned |
| 12 | Debug allocator and bounds checks | Canaries around allocations and checked array indexes in debug builds. | S | Planned |
Effort: S is days, M is weeks, L is a project. Ranked by payoff for effort.
I64 i;
for (i=0;i<n;i++) {
if (!Keep(i))
goto skip; // no continue
Work(i);
skip:;
}There is no continue, so skipped iterations need labels, and each label is another place to slip.
I64 i;
for (i=0;i<n;i++) {
if (!Keep(i))
continue;
Work(i);
}The loop parsers already carry a break label, so adding a continue label is a small change. Planned, not built.
Speed and safety make HC+ better. These changes make it nicer to live in. Each one is judged by what it removes from real desktop code.
Bool LpMatch(I64 a)
{
I64 i,j;
U8 *s=ks_an[a];
if (!lp_ql) return TRUE;
for (i=0;s[i];i++) {
for (j=0;j<lp_ql && s[i+j] && LpLow(s[i+j])==LpLow(lp_q[j]);j++);
if (j==lp_ql) return TRUE;
}
return FALSE;
}Bool LpMatch(I64 a)
{
if (!lp_ql) return TRUE;
s := ks_an[a];
for (i in 0..StrLen(s)) {
j := 0;
while (j<lp_ql && s[i+j] && LpLow(s[i+j])==LpLow(lp_q[j])) j++;
if (j==lp_ql) return TRUE;
}
return FALSE;
}Before, then with HC+
The declaration block and the loop header shrink to what you mean.
No I64 i,j; block, and the empty-bodied inner for becomes a while that says what it does. := declares a variable where it is first used and takes its type from the value.
for (n=0;n<lp_nv;n++) {
target=1.0;
if (id==ID_LP+n || (n==lp_sel && id<ID_LP)) target=1.16;
old=lp_sc[n];
if (old<1.0) old=1.0;
if (!ks_anim) lp_sc[n]=target;
else lp_sc[n]=old+(target-old)*0.35;
if (AbsI64((lp_sc[n]-old)*1000)>0) LpDirtyTile(n);
}for (n in 0..lp_nv) {
hot := id==ID_LP+n || (n==lp_sel && id<ID_LP);
target := hot ? 1.16 : 1.0;
old := lp_sc[n] < 1.0 ? 1.0 : lp_sc[n];
lp_sc[n] = ks_anim ? old+(target-old)*0.35 : target;
if (Abs(lp_sc[n]-old) > 0.001) LpDirtyTile(n);
}Before, then with HC+
Five lines of set, test, set again become four lines that read in order.
Naming hot makes the intent obvious, and the AbsI64(x*1000)>0 trick becomes a plain comparison. HolyC has no ?: today.
if (lp_ql) StrPrint(buf,"%s",lp_q); else StrPrint(buf,"Type to search");
RRFill(x,MBAR+52,w,36,18,0xFFFFFF,40*fa/256);
LIcon(L_GRID,x+22,MBAR+70,16,0xF4F4F6,fa);
Txt(x+40,MBAR+60,F_REG,0xB8B8C4,buf,fa);#define LP_TEXT #F4F4F6
#define LP_DIM #B8B8C4
text := lp_ql ? lp_q : "Type to search";
RRFill(x, MBAR+52, w, 36, 18, #FFFFFF, 40*fa/256);
LIcon(L_GRID, x+22, MBAR+70, 16, LP_TEXT, fa);
Txt(x+40, MBAR+60, F_REG, LP_DIM, text, fa);Before, then with HC+
Colours look like colours, and repeated values get names.
The buf[80] array existed only to get a string pointer, so it and the if/else go away. About 40 hex literals in this one file would become named or recognisable colours.
if (ks_wtdraw) (*ks_wtdraw)();
...
if (ks_wtframe) (*ks_wtframe)(dt);ks_wtdraw?.();
...
ks_wtframe?.(dt);Before, then with HC+
A hook that may or may not be set takes one token, not two checks.
The launchpad file has four of these hooks already. ?. means "call it if it is set".
U0 LpMove(I64 d)
{
I64 old=lp_sel;
if (!lp_nv) return;
lp_sel=ClampI64(lp_sel+d,0,lp_nv-1);
Print("LpMove d=%d old=%d new=%d (03g:%d)\n",d,old,lp_sel,__LINE__);
if (old!=lp_sel) { LpDirtyTile(old); LpDirtyTile(lp_sel); }
}U0 LpMove(I64 d)
{
if (!lp_nv) return;
old := lp_sel;
lp_sel = Clamp(lp_sel+d, 0, lp_nv-1);
dbg d, old, lp_sel; // d=1 old=2 lp_sel=3 (03g-launchpad.HC:158)
if (old != lp_sel) { LpDirtyTile(old); LpDirtyTile(lp_sel); }
}
test "LpMove stays inside the list" {
lp_nv = 5; lp_sel = 4;
LpMove(3);
assert lp_sel == 4;
}Before, then with HC+
Debugging and tests become one line, and live next to the code.
dbg prints names, values and file:line from one line. A test block is run by the serial test harness and stripped from release builds.
The "before" code is taken from desk3/03g-launchpad.HC. The "after" uses proposed syntax: none of it exists yet.
One formatter with no options, a small language, and tooling that ships with it. HC+ takes the formatter and the restraint.
Errors that show the line, the reason and a fix. This is the most transferable idea, and patching the compiler's messages is cheap with the three-second reload.
Blocks, methods on values and readable literals. HC+ takes lambdas, method calls on structs and units such as 250ms, but not the magic.
HC+ is plain HolyC plus a few extra tokens. A host pass rewrites the extras and passes everything else through byte for byte.
desk3/01-core.HC copied as-is
desk3/03g-launchpad.HCP --> hcpp --> 03g-launchpad.HC (generated)
|
send-to-vm.sh ships every .HC to the VM.HC files are never touched. .HCP files go through a host-side pass and come out as ordinary HolyC. Files convert one at a time and the build never breaks halfway.
Tokens such as :=, ?., dbg and test do not compile today, so a plain .HC file cannot be changed by accident. Strings, comments and DolDoc $ commands are skipped.
Every rewrite stays on its original line, so a compiler error at line 158 points at line 158 of the source, with no source maps.
.HC file and require the output to equal the input. If that passes, original HolyC is valid HC+ by construction.test. The pass warns on any collision... next to ...: HolyC already uses case 1...5, so the lexer must treat both explicitly.obj.fn(x) already means something. A text pass cannot tell them apart, so the first version would use a distinct spelling such as rect:Contains(p).Items 1 to 5: hot loops, SSE2 kernels, the serial test harness, C precedence and continue. All are small and each can be checked with the benchmark set.
A lightly patched compiler carried as a small patch series, hot kernels assembled on the host, host-side lint, formatter and benchmarks, and a fault policy for the desktop.
No LLVM front end, no HolyC-to-C transpiler and no ARM kernel. The gain over fixed HolyC is a few times at best, and the cost is the live edit loop.
Source: the repository note Improving HolyC on all of its deficits, which has the measurements, the compiler locations and what could not be verified. Numbers here are from that note; read it before relying on them.
HC+ is HolyC with fixes, so the reference applies.