-
Notifications
You must be signed in to change notification settings - Fork 0
/
tutorial.tsx
664 lines (583 loc) · 18.2 KB
/
tutorial.tsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
/***
* _____ _ _ _
* |_ _| _ _ | |_ ___ _ _ (_) __ _ | |
* | | | +| | | _| / _ \ | '_| | | / _` | | |
* _|_|_ \_,_| _\__| \___/ _|_|_ _|_|_ \__,_| _|_|_
* _|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|_|"""""|
* "`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'"`-0-0-'
*/
/*
Welcome to the Macromania tutorial. You can start reading this file without
knowing anything about Macromania. The tutorial walks you through all the
features of Macromania. It also happens to double as a test suite.
*/
import { assertEquals } from "../devDeps.ts";
///////////////////
// 1. The Basics //
///////////////////
/*
Macromania defines a type `Expression`. You give Macromania an `Expression`, and
it expands the expression into a string.
*/
import { Context, Expression } from "../mod.ts";
/*
The most basic expression is a string, which evaluates to itself:
*/
Deno.test("string expression", async () => {
const ctx = new Context();
const got = await ctx.evaluate("Hello, world!");
assertEquals(got, "Hello, world!");
});
/*
This snippet has demonstrated the fundamental working of Macromania. You create
a new `Context`, pass an `Expression` (in this case, a string) to its
`evaluate` function, and get the expanded expression back.
*/
/*
To evaluate a sequence of expressions and concatenate the results, use a
`FragmentExpression`:
*/
Deno.test("fragment expression", async () => {
const ctx = new Context();
const got = await ctx.evaluate({ fragment: ["Hello,", " world", "!"] });
assertEquals(got, "Hello, world!");
});
/*
This is all nice and dandy, but not worth the effort so far. What we still lack
are _macros_. A macro is a function that returns an expression.
*/
Deno.test("simple macro", async () => {
function Em({ children }: { children: Expression }): Expression {
return { fragment: ["*", children, "*"] };
}
const ctx = new Context();
const got = await ctx.evaluate(
Em({ children: "Hello, world!" }),
);
assertEquals(got, "*Hello, world!*");
});
///////////////
// Using JSX //
///////////////
/*
There is a good reason for the rather unconventional argument type of the
preceding `Em` macro: We can use
[jsx](https://react.dev/learn/writing-markup-with-jsx) in macromania.
*/
Deno.test("simple macro jsx", async () => {
function Em({ children }: { children: Expression }): Expression {
return <>*{children}*</>; // A jsx fragment compiles into a FragmentExpression
}
const ctx = new Context();
const got = await ctx.evaluate(<Em>Hello</Em>);
assertEquals(got, "*Hello*");
});
/*
To use jsx, you need to configure your typescript compiler:
```json
{
"compilerOptions": {
"jsx": "react-jsxdev",
"jsxImportSource": "macromaniajsx",
},
"imports": {
"macromaniajsx/jsx-dev-runtime": "path/to/macromanias/mod.ts",
}
}
```
*/
/*
Remember that jsx only works with macros whose name starts with a capital
letter. Lowercase namess are reserved for built-in macros.
*/
/*
The `args` key of the single argument of each macro determines how many
expressions can go between the opening and closing tag of a macro invocation:
*/
Deno.test("jsx two children", async () => {
function Greet(
{ children }: { children: [Expression, Expression] },
): Expression {
return <>{children[0]}, {children[1]}!</>;
}
const ctx = new Context();
const got = await ctx.evaluate(
<Greet>
{"Hello"}
{"World"}
</Greet>,
);
assertEquals(got, "Hello, World!");
});
Deno.test("jsx two or more children", async () => {
function Join(
{ children }: { children: Expression[] },
): Expression {
return children.join("::");
}
const ctx = new Context();
const got = await ctx.evaluate(<Join>foo{"bar"}baz</Join>);
assertEquals(got, "foo::bar::baz");
});
Deno.test("jsx no children", async () => {
function Flubb(): Expression {
return "Flubb!";
}
const ctx = new Context();
const got = await ctx.evaluate(<Flubb />);
assertEquals(got, "Flubb!");
});
/*
Any argument key other than `children` can be used to define props:
*/
Deno.test("jsx props", async () => {
function Exclaim(
{ repetitions, children }: { repetitions: number; children: Expression },
): Expression {
return <>{children}{"!".repeat(repetitions)}</>;
}
const ctx = new Context();
const got = await ctx.evaluate(<Exclaim repetitions={3}>Hi</Exclaim>);
assertEquals(got, "Hi!!!");
});
////////////////////////
// Impure Expressions //
////////////////////////
/*
So far, we can basically concatenate strings, create them with functions, and
use jsx. That is not particularly impressive. But we are now ready to delve
into the reason why Macromania is so powerful: macros can be stateful, one
invocation of a macro can influence other invocations.
As a first demonstration, we define a simple counter macro that evaluates to an
incrementing number each time.
*/
import { createSubstate } from "../mod.ts";
Deno.test("counter macro", async () => {
// Create a getter and a setter for some macro-specific state.
const [getCount, setCount] = createSubstate<number /* type of the state*/>(
() => 0, // function producing the initial state
);
function Count(): Expression {
return {
// An impure expression maps a Context to an expression.
impure: (ctx: Context) => {
const oldCount = getCount(ctx);
setCount(ctx, oldCount + 1);
return `${oldCount}`;
},
};
}
const ctx = new Context();
const got = await ctx.evaluate(
<>
<Count /> <Count /> <Count />
</>,
);
assertEquals(got, "0 1 2");
});
/*
This example introduced a new kind of expression: an _impure_ expression is an
object with a single field `impure`, whose value is a function that takes a
`Context` and returns an expression. This function can read to and write from
the `Context` via getters and setters created by the `createSubstate` function.
The same `Context` is passed to separate invocations of the `Count` macro,
mutating the state allows earlier invocations to pass information to later
invocations.
*/
/*
Impure expressions are mostly used as an internal representation for Macromania.
Macro authors would typically use the `impure` intrinsic for creating them; here
is an example where we build a simple system of definitions and references:
*/
Deno.test("defs and refs", async () => {
// Create a getter and a setter for our state: a mapping from short ids to
// links.
const [getDefs, _setDefs] = createSubstate<Map<string, string>>(
() => new Map(), // the initial state
);
// Registers a short name for a link, does not produce any output.
function Def({ name, link }: { name: string; link: string }): Expression {
const updateState = (ctx: Context) => {
getDefs(ctx).set(name, link);
return "";
};
return <impure fun={updateState} />;
}
// Given a registered name, outputs the associated link.
function Ref({ name }: { name: string }): Expression {
const fun = (ctx: Context) => {
const link = getDefs(ctx).get(name);
if (link) {
return `<a href="${link}">${name}</a>`;
} else {
ctx.log(`Undefined name ${name}.`);
ctx.halt();
return "";
}
};
return <impure fun={fun} />;
}
const ctx1 = new Context();
const got1 = await ctx1.evaluate(
<>
<Def name="squirrel" link="https://en.wikipedia.org/wiki/Squirrel" />
Look, a <Ref name="squirrel" />!
</>,
);
assertEquals(
got1,
`Look, a <a href="https://en.wikipedia.org/wiki/Squirrel">squirrel</a>!`,
);
/*
This example also demonstrates two methods of `Context`: `log` prints output
to a console, and `halt` aborts macro expansion.
These macros require definitions to occur before references, otherwise
evaluation fails:
*/
const ctx2 = new Context();
const got2 = await ctx2.evaluate(
<>
<Ref name="squirrel" /> ahead!
<Def name="squirrel" link="https://en.wikipedia.org/wiki/Squirrel" />
</>,
);
assertEquals(got2, null);
/*
We can fix this by returning `null` from the impure function in the Ref macro
if the name has not been defined yet. Returning `null` causes the
expression to be skipped and schedules it for later reevaluation.
*/
// Given a name, outputs the associated link. Asks to be reevaluated at a
// later point if the name is not yet defined.
function PatientRef({ name }: { name: string }): Expression {
const fun = (ctx: Context) => {
const link = getDefs(ctx).get(name);
if (link) {
return `<a href="${link}">${name}</a>`;
} else {
return null;
}
};
return <impure fun={fun} />;
}
const ctx3 = new Context();
const got3 = await ctx3.evaluate(
<>
<PatientRef name="squirrel" /> ahead!
<Def name="squirrel" link="https://en.wikipedia.org/wiki/Squirrel" />
</>,
);
assertEquals(
got3,
`<a href="https://en.wikipedia.org/wiki/Squirrel">squirrel</a> ahead!`,
);
/*
If we reference a name that never gets defined, the evaluator recognizes that
macro expansion stops making progress at some point, the `evaluate` method then
returns `null` to indicate failure. That's a lot nicer than going into an
infinite loop.
*/
const ctx4 = new Context();
const got4 = await ctx4.evaluate(
<>
<PatientRef name="squirrel" />!
</>,
);
assertEquals(got4, null);
assertEquals(ctx4.didGiveUp(), true);
/*
When the evaluator detects that evaluation does not progress, it attemps
reevaluation a final time. Macros can query whether they are being evaluated in
a final attempt:
*/
function MostlyPatientRef({ name }: { name: string }): Expression {
const fun = (ctx: Context) => {
const link = getDefs(ctx).get(name);
if (link) {
return `<a href="${link}">${name}</a>`;
} else {
if (ctx.mustMakeProgress()) {
ctx.log("Unknown name: ", name);
return "[unknown name]";
} else {
return null;
}
}
};
return <impure fun={fun} />;
}
const ctx5 = new Context();
const got5 = await ctx5.evaluate(
<>
<MostlyPatientRef name="squirrel" /> ahead!
</>,
);
assertEquals(got5, `[unknown name] ahead!`);
});
///////////////////////////
// Lifecycle Expressions //
///////////////////////////
/*
The `lifecycle` intrinsic allows us to wrap an expression with functions that
are called for their side effects before or after the wrapped expression gets
evaluated.
In the following example, we build a macro for nested markdown sections that
automatically uses the correkt markup for headings.
*/
// Create a getter and a setter for section depth.
Deno.test("nested markdown sections", async () => {
const [getDepth, setDepth] = createSubstate<number>(() => 0);
// Render the markup for a heading.
function AutoHeading(
{ children }: { children: Expression },
): Expression {
const fun = (ctx: Context) => {
return <>{"#".repeat(getDepth(ctx))} {children}{"\n"}</>;
};
return <impure fun={fun} />;
}
// Render the markup for a section.
function Section(
{ children, title }: { children: Expression; title: Expression },
): Expression {
const pre = (ctx: Context) => setDepth(ctx, getDepth(ctx) + 1);
const post = (ctx: Context) => setDepth(ctx, getDepth(ctx) - 1);
return (
<lifecycle pre={pre} post={post}>
<>
<AutoHeading>{title}</AutoHeading>
{children}
</>
</lifecycle>
);
}
const ctx = new Context();
const got = await ctx.evaluate(
<Section title="My Text">
<>
Bla bla bla{"\n"}
<Section title="Subsection 1">{"Hi!\n"}</Section>
<Section title="Subsection 2">{"Bye!\n"}</Section>
</>
</Section>,
);
assertEquals(
got,
`# My Text
Bla bla bla
## Subsection 1
Hi!
## Subsection 2
Bye!
`,
);
});
/*
Note that the lifecycle functions get called _every time_ the wrapped expression
is evaluated; this gracefully handles wrapped impure expressions that require
several evaluation attempts.
*/
/////////////////////
// Map Expressions //
/////////////////////
/*
The third and last major kind of expressions are _mapping expressions_, which
are created via the `map` intrinsic. They wrap an expression, and once that
expression has been evaluated to a string, it is given to a function that can
turn it into an arbitrary new expression.
*/
Deno.test("yell", async () => {
function Yell({ children }: { children: Expression }): Expression {
const fun = (evaled: string, ctx: Context) => evaled.toUpperCase();
return <map fun={fun}>{children}</map>;
}
const ctx = new Context();
const got = await ctx.evaluate(<Yell>Help!</Yell>);
assertEquals(got, "HELP!");
});
///////////////////
// Miscellaneous //
///////////////////
/*
The `<omnomnom>` intrinsic wraps an expression. This expression gets evaluated for
any side-effects, but the intrinsic then evaluates to the empty string.
*/
Deno.test("omnomnom", async () => {
const ctx = new Context();
const got = await ctx.evaluate(
<omnomnom>Actually, I believe the artist *really* wants to...</omnomnom>,
);
assertEquals(got, "");
});
/*
The `<halt />` intrinsic erroneously halts evaluation when evaluated.
*/
Deno.test("halt intrinsic", async () => {
const ctx = new Context();
const got = await ctx.evaluate(
<halt />,
);
assertEquals(got, null);
});
/*
The `<fragment>` intrinsic can be used to convert an array of Expressions into
a FragmentExpression:
*/
Deno.test("fragment intrinsic", async () => {
const ctx = new Context();
const got = await ctx.evaluate(
<fragment exps={["a", "b", "c"]} />,
);
assertEquals(got, "abc");
});
/*
If you want to define a macro that can operate on any number of children, then
the typechecker start complaining because of the way that jsx gets compiled.
Use the `Expressions` type and the `expressions` function to work around it.
*/
import { Expressions, expressions } from "../mod.ts";
Deno.test("many children", async () => {
function Many({ children }: { children?: Expressions }): Expression {
const inner = expressions(children);
return `${inner.length}`;
}
const ctx1 = new Context();
const got1 = await ctx1.evaluate(<Many />);
assertEquals(got1, "0");
const ctx2 = new Context();
const got2 = await ctx2.evaluate(<Many>foo</Many>);
assertEquals(got2, "1");
const ctx3 = new Context();
const got3 = await ctx3.evaluate(<Many>{"foo"}{"bar"}{"baz"}</Many>);
assertEquals(got3, "3");
});
/*
If you need to treat some `Expressions | undefined` as a single `Expression`,
the `<exps />` intrinsic has you covered:
*/
Deno.test("exps", async () => {
function ExpsDemo({ children }: { children?: Expressions }): Expression {
return <exps x={children} />
}
const ctx1 = new Context();
const got1 = await ctx1.evaluate(<ExpsDemo />);
assertEquals(got1, "");
const ctx2 = new Context();
const got2 = await ctx2.evaluate(<ExpsDemo>foo</ExpsDemo>);
assertEquals(got2, "foo");
const ctx3 = new Context();
const got3 = await ctx3.evaluate(<ExpsDemo>{"foo"}{"bar"}{"baz"}</ExpsDemo>);
assertEquals(got3, "foobarbaz");
});
//////////////////
// Async Macros //
//////////////////
/*
The `evaluate` method of `Context` is asynchronous. The functions of any impure, lifecycle or map expression can be asynchronous and everything still works!
*/
import { encodeHex } from "../devDeps.ts";
Deno.test("async map", async () => {
function Sha256({ children }: { children: Expression }): Expression {
return (
<map
fun={async (evaled, ctx) => {
// Yes, for some mysterious reason, WebCrypto hash functions are async.
const rawDigest = await crypto.subtle.digest(
"SHA-256",
new TextEncoder().encode(evaled),
);
return encodeHex(rawDigest);
}}
>
{children}
</map>
);
}
const ctx = new Context();
const got = await ctx.evaluate(<Sha256>Hello, world!</Sha256>);
assertEquals(
got,
"315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3",
);
});
/*
Note that using async functions in Macromania does not automatically result in concurrent macro evaluation; fragments still evaluate everything sequentially:
*/
function sleep(milliseconds: number): Promise<void> {
return new Promise((r) => setTimeout(r, milliseconds));
}
Deno.test("sequential async fragments", async () => {
const ctx = new Context();
let counter = 0;
const got = await ctx.evaluate(
<>
<impure
fun={async (ctx) => {
await sleep(1); // really fast
counter += 1;
return `${counter}`;
}}
/>
<impure
fun={async (ctx) => {
await sleep(99); // really slow
counter += 1;
return `${counter}`;
}}
/>
<impure
fun={async (ctx) => {
await sleep(50); // in between
counter += 1;
return `${counter}`;
}}
/>
</>,
);
assertEquals(got, "123");
// Takes 150 milliseconds to evaluate.
});
/*
To evaluate macros concurrently (but still concatenate the resuls in the order
in which the macros were given), use the `<concurrent>` intrinsic:
*/
Deno.test("concurrent macros", async () => {
const ctx = new Context();
let counter = 0;
// The only change to the previous example that we use `<concurrent>` instead
// of `<>`
const got = await ctx.evaluate(
<concurrent>
<impure
fun={async (ctx) => {
await sleep(1); // really fast
counter += 1;
return `${counter}`;
}}
/>
<impure
fun={async (ctx) => {
await sleep(99); // really slow
counter += 1;
return `${counter}`;
}}
/>
<impure
fun={async (ctx) => {
await sleep(50); // in between
counter += 1;
return `${counter}`;
}}
/>
</concurrent>,
);
assertEquals(got, "132");
// Takes 99 milliseconds to evaluate.
});
/*
And that concludes the tutorial.
To develop a complete understanding of Macromania's workings, We recommend
reading the source code. It is pretty straightforward, well-commented, and not
much longer than this tutorial.
Have fun building some macros!
*/