-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplayground.html
More file actions
1641 lines (1502 loc) · 69.9 KB
/
Copy pathplayground.html
File metadata and controls
1641 lines (1502 loc) · 69.9 KB
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
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>lua2wasm playground</title>
<!-- Favicon: the official Lua logo (http://lua-users.org/wiki/LuaLogo),
inlined as base64 PNGs at 16/32/64 px so the page stays self-contained
(the GitHub Pages deploy stages a flat dist/ with no asset copy step).
The browser picks the size closest to its render target. -->
<link rel="icon" type="image/png" sizes="16x16" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAABAAAAAQBPJcTWAAAAB3RJTUUH1wQKDjopNFerjAAAAnpJREFUeJxtk89LVFEcxT/3vTvP54w/xlFnIEfNVMQyalNYpCBhUNQi2rfPbQtD6B/QllLUwn0bV4aLpDa1iiCoJgk1E8QxUx/Nszfz5r17W1xFE7+rC/eee77ne77H5sQSApIZcNNQUwduI1g2xBVznz0H3aMQeOIY0IJELThJiCoQhaA1WBbYCbAd8+mtp5C9AEuv5CHYkoZNxRD8gUw35M6D2wTBDvz6ArvL0HYF2odAutB9Ux4y19QZ1mQWrj6EvruQypnWVWxZf4taL85q/X0OthehuQ+KH/clOCmj222GOy/g9HUDNNXf38LY2GWCIIynp1++XlubfwKWA78Lcl+3C2XfMP8PltJifPwa9+9fRCltx7EenZhY+qb1wiOobbTMhOPIaO67exR84IfjyP2zwHGkDf33oKkLooqERBL+7kB2AFLZgYEsq6sevh8CUK0qpqbeEceKIIh49uwDWqdy0HIWvJ8SdARagZsBIQcH83hegO+H9PZmWF8vUSqFTE6+p6eniWo1BoSE2gxobQHCNFreMZ8JQCAEDA93ksm4DA11ks/X09qaYmSkC1CRsdayJMQhSAc2P8PeZiJhtbe3NwDgupJcro502qW+3sHzyqRSCWBvA7a+gp2wISqD2wClDWhokzJ/qaMjbSWTCYpFn7a2BgqFLTyvghACzwuilZW3M0oVZsGpPdiDJAgbatJw+zmcuXHcDVMqguV5mHsAYQlUtP9IxSYDoQ8/Fszu158yDgkBKoTSOnyagTePobwLtoRw70iYjmYhCqHpNLSeNe4E27BVAG/VLJ0QUPFBxSel0TXMcdUMWCvjlHRMgCq+mZtWAP8Azi3klkVk/CYAAAAASUVORK5CYII=">
<link rel="icon" type="image/png" sizes="32x32" href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAABmJLR0QAAAAAAAD5Q7t/AAAACXBIWXMAAAABAAAAAQBPJcTWAAAAB3RJTUUH1wQKDjogTYsTKAAABs1JREFUeJydl1lsXNUZx3935s6MZ7Vn7BnvYycxWcZ2SBxEmiZuU4mWpOSFEIEE6QuCClGptC+NQIiqPFS8IMEbolKFeGnAimpE1IpNbVDTSiYhQYwdOwpOYryOPZ599cy9ffiuMzPJ2HH7l650zznfOd/+ne+Y+b/R0AhNPVBIQWMnmC2wlt18j9UBNheUS6BrAOrWGZpUMJmh+yFYuQGKGexNEFfA4QPNDaUctA3A4gQU0oAue1Ub7HwMQk+C3QvL43Dlz7AUVrbGvKUPXH6YuSRaFNKgl++lUxRhUMyAv0/4L12Dh34JP/kjWBsrtJGrMHravDnjwG4xWXIe4rOglaBUqGhWD2s50MqQi0O5KK569G1wB2vpnK1QymzggnVzl4uQWoK1fO2aKwBN28AZAJMFtCKkFyExA+ll0NagvAbZVWjbD+7OOkwUaBmoJ4ACwYMQuwXR6cq01QU9R2DgKWh/GFydoDaAYpKAKmUhvQDzYxD+C8xcFFcUU1BMg817L6vc8l0xoJgkYFQb5BKALnPt++DwGdj2KFjd9a1WjWISpv8O/3oDVqbgp2/A0K8kcNeRX4HRX9wVAy07wNMGsduGuc0Qehweewc6j4DZtjljBYfDitNpt2lay0C5HDwKiZswPgKqKn7X1iA6AV/+Aab+ptTsxu4Rf5cKEtH9p+Bnb4Oj9X4622wWTp0K8cwzD9LcbGd6Osa7717iwoXwnKadfwG++xw8HWBxQCYCmSigV1mg40HIJ8VfAB1D8PN3wN19P+aKYuLZZ4d4663jhEJ+Ojrc9PcHOHp0G5cvr3hmZuxDcPsfsHIdMsuyy9sNuZipon0pL1UNJOAOn4HG7fdjDtDc7OD55w/gdltr5oPBRp577gCq6t8NP/ydaA+Szp42UEyGAA0eWL0tCwC9P4Ltx7bCHMDvd9DV5am71tfnw263AA+cgO4fyGy5CDNfgVk1BGgbkAoHkuehU2BxbVWAeDxPNJqru7awkKZQKCNVsP/JSibYPNA1ZAiw8A1kY/LvaoXOQ1tlDhCJZPjgg7DBqIJoNMd7712hWDQMS+chcLbIfz4B0ZtmqWqNnZCOyEJgAA68ICmncPBgN16vnUgks6EAug5Xry6QShVpbXWytqbx7bcRXn/9n3z88SSaphmUZhWmRqVq6ho0NKpGKdUqxzn9UuEETz89yK1bCcLhpU1soJNM5nnzzYu8//4VXC4r0WiOeDxP7bWhOsHhr4wbu0ySk6vfVRHZgfXsQFFA12vvHpPJxN69bbjdUpd8PgehUABN08nnS+zY0cyJEzsZHu7BZlNrdhrnG1j4xgTeXujYX5ks5YEqi9wLm03lzJlhent9AOzb185LLx2iocHCK6/8mNOn9xIMNvHaa0c5fnxntaU0KBcq49Z+FVKLUEhWJjNL0lhYLJsJYbGYURTF0EsxxjA2NsvY2BzpdAGXy0oo5Gd0dH1XMS3+X0d5TZXi0+CR9NDLEJ+B1Dz46ie2AUWRrxqapqNpOi+/PAxAKBTgs8+qvEtqFlJzxgkmiE0bvvYGweY0LBCBuf/UMlOA6k+Ym0wybm62o6oKwWATL774MCMj47z66hecPz91h0YwexGyUfl3+KA1ZETI95e5E65aCcJnYdcTYPPousKxY334fJIY5bLOyMg42WyJkyf3EAg4OHkyRD5fQtd1FEWhpcXB8HAPjzyyndnZJC6XlXQ6GYPxD9ebUXHH0qS5YtDeQ5CYF4L0Ivh3QWAwkcgTiWSIx/PE43lisTyTkyt8/fU8g4MB2tvdnDs3wdjYHOPjEZaW0hw+HMRmM/PRR1N4vXYmJiJ6Mnn1Q7j0J1HQbJWrPzFX5UVvUNqvkhGlbXvhibPg3bVBFNw11u/MS+pWVNP15TCcewqWJw3ze8EdgKWp6g4lIVdkqShNQzoCqRkIDoN104DcHKnb8MmvYfYrGVvs0ujE5wDu6oicLXIp5eIyjt6A2JS0ZPaWOmpvBh1WwvDpb6QZWTdR5z6pvPl4HQGyq/K6aQpKc6JrIsTMl9IteYLVZXpjFGJw7Sx8+lv4fow7vaXTJ+19dnWdss67QAfa9ogAJaMdz0Tgxicw928oZyWIzKocii4uKyZg9Tpc/ytc+L0EXLrqAvEGwdcLq7equW1kUgUsNmjdA4vXKoKA+M/uA0+XvAvMDdKSZ5al0GRjta8mqwsa24WxVq6k4eYCGGveIORicjXnE9LJbBWqTYR1+QETrN7c+t57BGkfhNbdEsEuv2H6OjCpUuHMFth+BHw9Wzj8f0GDR96Li2Hw7RBTR6ehcz/EDA2bgjB3RSK93gO2Fv8FRoh2GeGG+4oAAAAASUVORK5CYII=">
<link rel="icon" type="image/png" sizes="64x64" href="data:image/png;base64,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">
<style>
/* Catppuccin palettes (https://github.com/catppuccin/catppuccin).
Two flavours wired to the same variable names so the rest of the
stylesheet — and the CodeMirror theme below — only references
--ctp-* and just follows whichever block is active. Mocha is the
default; setting `data-theme="light"` on <html> swaps to Latte. */
:root, html[data-theme="dark"] {
color-scheme: dark;
--ctp-rosewater: #f5e0dc;
--ctp-flamingo: #f2cdcd;
--ctp-pink: #f5c2e7;
--ctp-mauve: #cba6f7;
--ctp-red: #f38ba8;
--ctp-maroon: #eba0ac;
--ctp-peach: #fab387;
--ctp-yellow: #f9e2af;
--ctp-green: #a6e3a1;
--ctp-teal: #94e2d5;
--ctp-sky: #89dceb;
--ctp-sapphire: #74c7ec;
--ctp-blue: #89b4fa;
--ctp-lavender: #b4befe;
--ctp-text: #cdd6f4;
--ctp-subtext1: #bac2de;
--ctp-subtext0: #a6adc8;
--ctp-overlay2: #9399b2;
--ctp-overlay1: #7f849c;
--ctp-overlay0: #6c7086;
--ctp-surface2: #585b70;
--ctp-surface1: #45475a;
--ctp-surface0: #313244;
--ctp-base: #1e1e2e;
--ctp-mantle: #181825;
--ctp-crust: #11111b;
}
html[data-theme="light"] {
color-scheme: light;
--ctp-rosewater: #dc8a78;
--ctp-flamingo: #dd7878;
--ctp-pink: #ea76cb;
--ctp-mauve: #8839ef;
--ctp-red: #d20f39;
--ctp-maroon: #e64553;
--ctp-peach: #fe640b;
--ctp-yellow: #df8e1d;
--ctp-green: #40a02b;
--ctp-teal: #179299;
--ctp-sky: #04a5e5;
--ctp-sapphire: #209fb5;
--ctp-blue: #1e66f5;
--ctp-lavender: #7287fd;
--ctp-text: #4c4f69;
--ctp-subtext1: #5c5f77;
--ctp-subtext0: #6c6f85;
--ctp-overlay2: #7c7f93;
--ctp-overlay1: #8c8fa1;
--ctp-overlay0: #9ca0b0;
--ctp-surface2: #acb0be;
--ctp-surface1: #bcc0cc;
--ctp-surface0: #ccd0da;
--ctp-base: #eff1f5;
--ctp-mantle: #e6e9ef;
--ctp-crust: #dce0e8;
}
* { box-sizing: border-box; }
html, body { height: 100%; margin: 0; }
body {
display: grid;
grid-template-rows: auto auto 1fr auto;
font: 14px/1.4 system-ui, sans-serif;
background: var(--ctp-base);
color: var(--ctp-text);
}
#intro {
padding: .55em 1em; font-size: 13px; line-height: 1.5;
color: var(--ctp-subtext1);
background: var(--ctp-mantle);
border-bottom: 1px solid var(--ctp-surface0);
}
#intro strong { color: var(--ctp-text); }
#intro a { color: var(--ctp-sapphire); text-decoration: none; }
#intro a:hover { text-decoration: underline; }
header {
display: flex; align-items: center; gap: 1em;
padding: .6em 1em;
background: var(--ctp-mantle);
border-bottom: 1px solid var(--ctp-surface0);
}
header h1 { font-size: 1em; margin: 0; color: var(--ctp-subtext0); font-weight: 500; }
header h1 a { color: var(--ctp-lavender); text-decoration: none; }
header h1 a:hover { text-decoration: underline; }
button, select {
background: var(--ctp-surface0); color: var(--ctp-text);
border: 1px solid var(--ctp-surface1);
padding: .4em .9em; border-radius: 6px; font: inherit; cursor: pointer;
}
button:hover, select:hover { background: var(--ctp-surface1); border-color: var(--ctp-surface2); }
button[disabled] { opacity: .4; cursor: default; }
main {
/* Two flexible columns with a draggable splitter between them. The
JS sets --split-left (a percent like "42%") on pointermove; the
fr units fall back to the initial 50/50 split before any drag. */
display: grid;
grid-template-columns: minmax(160px, var(--split-left, 1fr))
6px
minmax(160px, 1fr);
min-height: 0;
border-bottom: 1px solid var(--ctp-surface0);
}
#editor-host {
overflow: auto;
background: var(--ctp-base);
}
.cm-editor { height: 100%; }
.cm-scroller { font: 13px/1.5 "JetBrains Mono", ui-monospace, monospace; }
#splitter {
background: var(--ctp-surface0);
cursor: col-resize;
/* Widen the hit target without enlarging the visible bar — a
transparent pseudo-element extends the grabbable area to ~10px. */
position: relative;
touch-action: none;
}
#splitter::before {
content: ""; position: absolute; inset: 0 -3px;
}
#splitter:hover,
#splitter.dragging { background: var(--ctp-surface1); }
/* While dragging: kill text selection across the whole window and
keep the resize cursor regardless of what the pointer is over. */
body.splitter-dragging { cursor: col-resize; user-select: none; }
#out {
margin: 0; padding: 1em; overflow: auto;
font: 13px/1.5 ui-monospace, monospace;
background: var(--ctp-crust); color: var(--ctp-text);
white-space: pre-wrap;
}
/* Scrollbars: blend into the Catppuccin Mocha surface tones rather
than the browser default light-on-dark.
Firefox honours scrollbar-color; Chromium/Safari use the webkit
pseudo-elements. Both target the editor (.cm-scroller) and the
output panel (#out). */
.cm-scroller, #out {
scrollbar-width: thin;
scrollbar-color: var(--ctp-surface1) transparent;
}
.cm-scroller::-webkit-scrollbar,
#out::-webkit-scrollbar {
width: 10px; height: 10px;
}
.cm-scroller::-webkit-scrollbar-track,
#out::-webkit-scrollbar-track {
background: transparent;
}
.cm-scroller::-webkit-scrollbar-thumb,
#out::-webkit-scrollbar-thumb {
background: var(--ctp-surface1);
border: 2px solid transparent; /* fakes inset padding around the thumb */
border-radius: 6px;
background-clip: padding-box;
}
.cm-scroller::-webkit-scrollbar-thumb:hover,
#out::-webkit-scrollbar-thumb:hover {
background: var(--ctp-surface2);
background-clip: padding-box;
}
.cm-scroller::-webkit-scrollbar-corner,
#out::-webkit-scrollbar-corner {
background: transparent;
}
/* WAT viewer: codegen tags each region with `;; @@SECTION:name@@`;
showWat() splits on those and emits one of these per section. The
runtime regions collapse by default since they're ~5000 lines of
shared infrastructure that doesn't change with the user's code. */
.wat-section { margin: 0 0 .25em 0; }
.wat-section > summary {
list-style: none;
cursor: pointer; user-select: none;
padding: .25em .6em; margin: 0;
background: var(--ctp-surface0); color: var(--ctp-subtext1);
border-radius: 4px;
font: 12px/1.4 system-ui, sans-serif;
display: flex; align-items: center; gap: .6em;
}
.wat-section > summary::-webkit-details-marker { display: none; }
.wat-section > summary::before {
content: "▸"; display: inline-block; width: 1em;
color: var(--ctp-overlay1); transition: transform .1s;
}
.wat-section[open] > summary::before { transform: rotate(90deg); }
.wat-section > summary .wat-name { color: var(--ctp-text); font-weight: 500; }
.wat-section > summary .wat-meta { color: var(--ctp-overlay1); font-size: 11px; margin-left: auto; }
.wat-section > pre {
margin: 0; padding: .4em .8em;
background: var(--ctp-base); color: var(--ctp-subtext1);
border-left: 2px solid var(--ctp-surface1);
border-radius: 0 0 4px 4px;
overflow-x: auto;
font: 12px/1.45 "JetBrains Mono", ui-monospace, monospace;
}
/* Highlight the "user code" section — that's the part the dev cares about. */
.wat-section[data-section="user-code"] > summary {
background: var(--ctp-surface1);
}
.wat-section[data-section="user-code"] > pre {
border-left-color: var(--ctp-sapphire);
color: var(--ctp-text);
}
/* WAT syntax tokens — palette mirrors the editor's Lua highlighter so
keywords/strings/comments/numbers all look the same on both sides. */
.wat-comment { color: var(--ctp-overlay1); font-style: italic; }
.wat-string { color: var(--ctp-green); }
.wat-number { color: var(--ctp-peach); }
.wat-keyword { color: var(--ctp-mauve); } /* func, param, result, etc. */
.wat-instr { color: var(--ctp-blue); } /* local.get, call, br_table, etc. */
.wat-type { color: var(--ctp-yellow); } /* i32, f64, anyref, … */
.wat-ident { color: var(--ctp-rosewater); } /* $name */
.wat-paren { color: var(--ctp-overlay2); }
/* A line belonging to a function/global that DCE drops from the final
.wasm. Dimmed + struck through so the surviving module reads clearly;
hover shows why. The opacity also fades the nested syntax spans. */
.wat-dead { opacity: .4; text-decoration: line-through;
text-decoration-color: var(--ctp-overlay0); }
.wat-note { margin: 0 0 .5em 0; color: var(--ctp-overlay1);
font: 12px/1.45 "JetBrains Mono", ui-monospace, monospace; }
/* "hide dead code" toggle next to the Show WAT button. */
#hidedead-label { color: var(--ctp-subtext0); font-size: 13px;
user-select: none; cursor: pointer;
display: inline-flex; align-items: center; gap: .3em; }
#hidedead-label input { vertical-align: middle; cursor: pointer; }
.err { color: var(--ctp-red); }
.info { color: var(--ctp-overlay1); }
.stdin-prompt { color: var(--ctp-sapphire); }
.stdin-input {
background: transparent; border: none; color: var(--ctp-text);
font: inherit; outline: none; min-width: 12em;
caret-color: var(--ctp-rosewater);
}
.stdin-done { color: var(--ctp-sky); }
footer {
padding: .4em 1em; font-size: 12px; color: var(--ctp-overlay1);
background: var(--ctp-mantle);
border-top: 1px solid var(--ctp-surface0);
}
footer code {
background: var(--ctp-surface0); color: var(--ctp-subtext1);
padding: 1px 4px; border-radius: 3px;
}
footer .warn {
background: var(--ctp-surface0); color: var(--ctp-yellow);
padding: 1px 6px; border-radius: 3px;
}
footer a { color: var(--ctp-sapphire); text-decoration: none; }
footer a:hover { text-decoration: underline; }
</style>
<header>
<h1><a href="https://github.com/rhmoller/lua2wasm" target="_blank" rel="noopener">lua2wasm</a> playground</h1>
<select id="preset">
<optgroup label="quick">
<option value="stack">stack (OO + errors + pcall)</option>
<option value="hello">hello</option>
<option value="fact">factorial</option>
<option value="counter">counter (closure)</option>
<option value="tables">tables</option>
<option value="oo">OOP via metatables</option>
<option value="pcall">pcall</option>
<option value="close"><close> (to-be-closed)</option>
<option value="forloop">for + ipairs</option>
<option value="iter">iterator (closure)</option>
<option value="stdin">interactive (io.read)</option>
<option value="files">files (io.open)</option>
</optgroup>
<optgroup label="from lua.org/extras">
<option value="s_hello">hello.lua</option>
<option value="s_account">account.lua</option>
<option value="s_bisect">bisect.lua</option>
<option value="s_globals">globals.lua</option>
</optgroup>
<optgroup label="tutorials">
<option value="tjdevries">TJ DeVries: Everything You Need To Start Writing Lua</option>
</optgroup>
</select>
<button id="run" disabled>Run ▶</button>
<button id="showwat">Show WAT</button>
<label id="hidedead-label"
title="Show WAT: strike through dead code, or omit it entirely">
<input type="checkbox" id="hidedead"> hide dead code
</label>
<span id="status" style="color:#888"></span>
<button id="theme-toggle" style="margin-left:auto"
title="Toggle Catppuccin Mocha (dark) / Latte (light)">
<span class="theme-icon">🌙</span>
</button>
</header>
<aside id="intro">
<strong>lua2wasm</strong> is an ahead-of-time compiler that turns Lua 5.5
source into standalone WebAssembly modules using the WASM-GC,
typed-references, and exception-handling proposals. The compiler itself
runs <em>in this page</em> — your Lua source never leaves the browser.
<a href="https://github.com/rhmoller/lua2wasm" target="_blank" rel="noopener">Source on GitHub →</a>
</aside>
<main>
<div id="editor-host"></div>
<div id="splitter" role="separator" aria-orientation="vertical"
tabindex="0" aria-label="Resize panels"></div>
<pre id="out"></pre>
</main>
<footer>
Pipeline: <code>Lua → lua2wasm (in WASM) → WAT → wat2wasm → WASM-GC → execute</code>
· Runs in any current browser (Chrome 137+, Firefox 131+, Safari 18.4+).
· color theme by <a href="https://catppuccin.com" target="_blank" rel="noopener">catppuccin.com</a>
· build <a id="build-hash" href="https://github.com/rhmoller/lua2wasm/commits/main" target="_blank" rel="noopener"><code>dev</code></a>
</footer>
<script type="module">
import { basicSetup, EditorView } from "https://esm.sh/codemirror@6.0.2";
import { StreamLanguage, HighlightStyle,
syntaxHighlighting } from "https://esm.sh/@codemirror/language@6.10.2";
import { lua as luaMode } from "https://esm.sh/@codemirror/legacy-modes@6.4.0/mode/lua";
import { tags as t } from "https://esm.sh/@lezer/highlight@1.2.1";
/* ---- Catppuccin theme via CSS variables ----
* The actual color values live in the :root / [data-theme] blocks at
* the top of this file. Pointing the CodeMirror theme at the same
* --ctp-* vars means a theme toggle just flips html[data-theme]; the
* editor follows along without rebuilding any extension. */
const CTP = new Proxy({}, { get: (_, k) => `var(--ctp-${String(k)})` });
const catppuccinTheme = EditorView.theme({
"&": { color: CTP.text, backgroundColor: CTP.base },
".cm-content": { caretColor: CTP.rosewater },
".cm-cursor, .cm-dropCursor": { borderLeftColor: CTP.rosewater },
"&.cm-focused .cm-selectionBackground, .cm-selectionBackground, .cm-content ::selection": {
backgroundColor: CTP.surface1 + " !important",
},
".cm-panels": { backgroundColor: CTP.mantle, color: CTP.text },
".cm-panels.cm-panels-top": { borderBottom: "2px solid " + CTP.surface0 },
".cm-panels.cm-panels-bottom": { borderTop: "2px solid " + CTP.surface0 },
".cm-searchMatch": { backgroundColor: CTP.surface2, color: CTP.text },
".cm-searchMatch.cm-searchMatch-selected": { backgroundColor: CTP.surface2 },
/* Tint the active line via a pseudo-element behind CodeMirror's
selection layer (drawSelection paints it at z-index -2, below
.cm-content). An opaque line background would sit in the content
layer and hide the selection on whichever line holds the selection
head — e.g. drag-selecting upward left the top line looking
unselected. z-index -3 keeps the tint below the selection so both
coexist. */
".cm-activeLine": { position: "relative" },
".cm-activeLine::before": {
content: '""', position: "absolute", inset: "0",
backgroundColor: CTP.mantle, zIndex: "-3", pointerEvents: "none",
},
".cm-selectionMatch": { backgroundColor: CTP.surface1 },
".cm-matchingBracket, .cm-nonmatchingBracket": {
backgroundColor: CTP.surface1, outline: "1px solid " + CTP.overlay0,
},
".cm-gutters": {
backgroundColor: CTP.base, color: CTP.overlay0,
borderRight: "1px solid " + CTP.surface0,
},
".cm-activeLineGutter": { backgroundColor: CTP.mantle, color: CTP.lavender },
".cm-foldPlaceholder": { backgroundColor: CTP.surface1, color: CTP.subtext0, border: "none" },
".cm-tooltip": {
backgroundColor: CTP.mantle, color: CTP.text,
border: "1px solid " + CTP.surface0,
},
".cm-tooltip-autocomplete > ul > li[aria-selected]": {
backgroundColor: CTP.surface1, color: CTP.text,
},
}, { dark: true });
const luaHighlightStyle = HighlightStyle.define([
{ tag: [t.keyword, t.controlKeyword, t.modifier, t.definitionKeyword], color: CTP.mauve, fontWeight: "500" },
{ tag: t.operatorKeyword, color: CTP.mauve },
{ tag: [t.string, t.special(t.string)], color: CTP.green },
{ tag: t.regexp, color: CTP.red },
{ tag: t.escape, color: CTP.pink },
{ tag: [t.number, t.bool, t.null, t.atom], color: CTP.peach },
{ tag: [t.comment, t.lineComment, t.blockComment], color: CTP.overlay0, fontStyle: "italic" },
{ tag: t.operator, color: CTP.sky },
{ tag: [t.punctuation, t.bracket, t.brace, t.paren], color: CTP.overlay2 },
{ tag: t.variableName, color: CTP.text },
{ tag: t.function(t.variableName), color: CTP.blue },
{ tag: t.function(t.propertyName), color: CTP.blue },
{ tag: t.propertyName, color: CTP.lavender },
{ tag: t.labelName, color: CTP.maroon },
{ tag: t.typeName, color: CTP.yellow },
{ tag: t.namespace, color: CTP.yellow },
{ tag: t.self, color: CTP.red, fontStyle: "italic" },
{ tag: t.invalid, color: CTP.red, textDecoration: "underline" },
]);
import { loadCompiler } from "./lua2wasm-wasm.mjs";
import { InMemoryFs } from "https://esm.sh/just-bash@latest";
import { MATH_FNS, MATH2_FNS,
makeHelpers, BufferedFile,
parseFileMode } from "./host-bindings.mjs";
import { formatFloat, formatScalar, cFormatG, cFormatF, cFormatE } from "./format.mjs";
/* ----- fixture presets ----- */
const PRESETS = {
// AUTO-GENERATED — edit runtime/presets/manifest.json and the
// referenced .lua files, then run `node scripts/bundle-presets.mjs`.
"stack": `-- A "Stack" class — exercises tables, metatables, OO sugar, closures,
-- errors, and pcall in one short program.
local Stack = {} -- a Lua table → a \`$LuaTable\` struct:
Stack.__index = Stack -- array part + open-addressed hash + \`meta\` slot
function Stack.new()
return setmetatable({n = 0}, Stack) -- writes Stack into the struct's \`meta\` field
end -- \`Stack.new\` itself is a \`(ref $LuaClosure)\` =
-- struct (funcref code, upvalue array)
function Stack:push(v) -- \`:\` desugars to \`self\` as the first parameter
self.n = self.n + 1 -- small ints unbox to \`i31ref\` — zero allocation
self[self.n] = v -- integer keys land in the dense array part
end
function Stack:pop()
if self.n == 0 then
error("stack underflow") -- compiles to \`throw $LuaError\` (anyref payload)
end
local v, n = self[self.n], self.n
self[n], self.n = nil, n - 1
return v
end
local s = Stack.new()
s:push("hi"); s:push("from"); s:push("lua2wasm")
-- each string literal materialises once via \`array.new_data\` from a shared data segment
for i = 1, s.n do print(s[i]) end -- numeric \`for\` keeps \`i\` as a raw \`i64\`;
-- the call to \`print\` dispatches via \`call_ref\`
local ok, err = pcall(function() -- \`pcall\` → \`try_table (catch $LuaError ...)\`
for _ = 1, 4 do s:pop() end -- the 4th \`pop\` underflows and throws
end)
print(ok, err) --> false <src>:19: stack underflow
`,
"hello": `print("hello from lua2wasm!")
print(1 + 2)
`,
"fact": `local function fact(n)
if n <= 1 then return 1 end
return n * fact(n - 1)
end
print(fact(10)) -- 3628800
`,
"counter": `local function counter()
local n = 0
local function tick() n = n + 1; return n end
return tick
end
local c = counter()
print(c()) -- 1
print(c()) -- 2
print(c()) -- 3
`,
"tables": `local t = {10, 20, 30, name = "alice"}
print(t[1])
print(t.name)
print(#t)
local nested = {inner = {x = 1, y = 2}}
print(nested.inner.x + nested.inner.y)
`,
"oo": `local Animal = {kind = "animal"}
Animal.speak = function(self) return self.kind .. " speaks" end
local Dog = setmetatable({kind = "dog"}, {__index = Animal})
print(Dog.speak(Dog))
-- vector with __add
local Vec = {}
Vec.__add = function(a, b)
return setmetatable({x = a.x + b.x, y = a.y + b.y}, Vec)
end
local v = setmetatable({x = 1, y = 2}, Vec) + setmetatable({x = 10, y = 20}, Vec)
print(v.x); print(v.y)
`,
"pcall": `local function safediv(a, b)
if b == 0 then error("division by zero") end
return a / b
end
print(pcall(safediv, 10, 2)) -- true 5.0
print(pcall(safediv, 1, 0)) -- false "division by zero"
`,
"close": `-- To-be-closed variables (Lua 5.4+): a local marked <close> runs its value's
-- __close metamethod the moment it leaves scope -- on normal exit, return,
-- break, goto, or an error. Think RAII: open a resource, and it's released
-- deterministically however the block ends.
-- A tiny "resource" whose __close just logs that it was released.
local function resource(name)
print("open " .. name)
return setmetatable({ name = name }, {
__close = function(self) print("close " .. self.name) end,
})
end
print("1) released at end of scope, in reverse order:")
do
local a <close> = resource("a")
local b <close> = resource("b")
print(" ...using a and b...")
end
print("\\n2) released even when an error unwinds the block:")
print(" pcall ->", pcall(function()
local r <close> = resource("r")
error("boom")
end))
print("\\n3) a generic-for's iterator can carry a closing value,")
print(" released when the loop ends (here: after a break):")
local function lines(n)
local i = 0
local iter = function() i = i + 1; if i <= n then return "line " .. i end end
return iter, nil, nil, resource("file")
end
for text in lines(99) do
print(" " .. text)
if text == "line 2" then break end
end
`,
"iter": `-- A "stateful closure as iterator" — the simplest form of a generic-for
-- iterator. numberGenerator returns a closure that captures \`start\` in a
-- mutable upvalue; each call advances it and returns either the next number
-- or nothing (which generic-for interprets as the end).
local function numberGenerator(start, stop)
return function()
start = start + 1
if start <= stop then
return start
end
end
end
-- prints 2, 3, 4, 5
for num in numberGenerator(1, 5) do
print(num)
end
-- A second instance has its own captured \`start\` — closures don't share.
local g = numberGenerator(10, 12)
print(g()) -- 11
print(g()) -- 12
print(g()) -- (no values; print emits a blank line — Lua splices call returns)
`,
"stdin": `-- Exercises io.read(); the test pipes two lines of stdin.
local first = io.read()
local second = io.read()
local third = io.read() -- EOF -> nil
print(first)
print(second)
print(third)
`,
"files": `-- Files, in your browser! io.open / io.lines / io.type read and write a
-- virtual filesystem (powered by just-bash) that PERSISTS across Runs.
-- Click Run a few times and watch the visit counter climb.
local counter = "/demo/visits.txt"
-- Read the previous count. io.open returns nil (not an error) when the
-- file is missing, so the very first Run simply starts from zero.
local n = 0
local f = io.open(counter, "r")
if f then
n = tonumber(f:read("a")) or 0
f:close()
end
n = n + 1
-- Write the new count back. "w" truncates; assert turns a failed open
-- into an error carrying the host's message.
local out = assert(io.open(counter, "w"))
out:write(tostring(n))
out:close()
print("You have run this program " .. n .. " time(s).")
-- A second file shows the rest of the API: multi-arg write, io.lines,
-- byte-accurate read + seek, io.type, and os.remove.
local notes = "/demo/notes.txt"
local g = assert(io.open(notes, "w"))
g:write("buy milk\\n")
g:write("write a compiler\\n", "learn Lua\\n") -- :write takes many args
g:close()
print("\\nnotes.txt, line by line:")
for line in io.lines(notes) do
print(" - " .. line)
end
local r = assert(io.open(notes, "r"))
print("\\nfirst 8 bytes:", r:read(8)) -- "buy milk"
r:seek("set", 0) -- rewind to the start
print("after rewind: ", r:read("l")) -- "buy milk" (whole first line)
print("io.type open: ", io.type(r)) -- file
r:close()
print("io.type closed:", io.type(r)) -- closed file
os.remove(notes)
print("\\nremoved notes.txt — visits.txt stays. That's the persistence!")
`,
"forloop": `local list = {"apple", "banana", "cherry"}
for i, v in ipairs(list) do
print(i)
print(v)
end
local sum = 0
for i = 1, 100 do sum = sum + i end
print(sum) -- 5050
`,
"s_hello": `-- the first program in every language (verbatim from lua.org/extras)
io.write("Hello world, from ",_VERSION,"!\\n")
`,
"s_account": `-- A simple example of OOP in Lua (from PiL 1, ch. 16).
-- Adapted for lua2wasm: only the \`global\` declarations have been added; the
-- rest is byte-for-byte the original from lua.org/extras.
global Account = {balance = 0}
function Account:new (o, name)
o = o or {name=name}
setmetatable(o, self)
self.__index = self
return o
end
function Account:deposit (v)
self.balance = self.balance + v
end
function Account:withdraw (v)
if v > self.balance then error("insufficient funds on account "..self.name) end
self.balance = self.balance - v
end
function Account:show (title)
print(title or "", self.name, self.balance)
end
global a = Account:new(nil,"demo")
a:show("after creation")
a:deposit(1000.00)
a:show("after deposit")
a:withdraw(100.00)
a:show("after withdraw")
`,
"s_bisect": `-- Bisection method for solving non-linear equations.
-- Adapted for lua2wasm: globals are explicitly declared up front; everything
-- else is byte-for-byte the original.
global delta = 1e-6
global n
global bisect
global solve
global f
function bisect(f,a,b,fa,fb)
local c=(a+b)/2
io.write(n," c=",c," a=",a," b=",b,"\\n")
if c==a or c==b or math.abs(a-b)<delta then return c,b-a end
n=n+1
local fc=f(c)
if fa*fc<0 then return bisect(f,a,c,fa,fc) else return bisect(f,c,b,fc,fb) end
end
function solve(f,a,b)
n=0
local z,e=bisect(f,a,b,f(a),f(b))
io.write(string.format("after %d steps, root is %.17g with error %.1e, f=%.1e\\n",n,z,e,f(z)))
end
function f(x)
return x*x*x-x-1
end
solve(f,1,2)
`,
"s_globals": `-- Adapted from lua.org/extras' globals.lua. The original walks Lua's \`_G\`
-- and uses table.sort; our compiler doesn't expose either, so this walks a
-- sample table instead and prints keys in insertion order (which our
-- linear-probe hash table happens to preserve).
local seen = {}
local function dump(t, indent)
seen[t] = true
for k, v in pairs(t) do
print(indent .. k)
if type(v) == "table" and not seen[v] then
dump(v, indent .. " ")
end
end
end
local sample = {
alpha = 1,
beta = "hi",
nested = {x = 10, y = 20},
gamma = true,
}
dump(sample, "")
`,
"tjdevries": `--[[
Examples from TJ Devries: "Everything You Need To Start Writing Lua"
https://www.youtube.com/watch?v=CuWfgiwI73Q
--]]
-- This is a comment, It starts with two dashes
--[[ This is also
a comment.
But it spans multiple lines!
--]]
local number = 5
local string = "hello, world"
local single = "also works"
local crazy = [[ This
is multie line and literal ]]
local truth, lies = true, false
local nothing = nil
local function hello(name)
print("Hello!", name)
end
local greet = function(name)
-- .. is string concatenation
print("Greetings, " .. name .. "!")
end
local higher_order = function(value)
return function(another)
return value + another
end
end
local add_one = higher_order(1)
print("add_one(2) -> ", add_one(2))
local list = {
"first",
2,
false,
function()
print("Fourth!")
end,
}
print("Yup, 1-indexed", list[1])
print("Fourth is 4 ... :", list[4]())
local t = {
literal_key = "a string",
["an expression"] = "also works",
[function() end] = true,
}
print("literal_key : ", t.literal_key)
print("an expression : ", t["an expression"])
print("function() end: ", t[function() end]) -- this print nil, but should print nothing
local favorite_accounts = { "teej_dv", "ThePrimeagen", "terminaldotshop" }
for index = 1, #favorite_accounts do
print(index, favorite_accounts[index])
end
for index, value in ipairs(favorite_accounts) do
print(index, value)
end
local reading_scores = { teej_dv = 9.5, ThePrimeagen = "N/A" }
for index = 1, #reading_scores do
print(reading_scores[index])
end
for key, value in pairs(reading_scores) do
print(key, value)
end
local function action(loves_coffee)
if loves_coffee then
print("Check out \`ssh terminal.shop\` - it's cool!")
else
print("Check out \`ssh terminal.shop\` - it's still cool!")
end
end
-- "falsey": nil, false
action()
action(false)
-- Everything else is "truthy"
action(true)
action(0)
action({})
--[[ these are wonky since we don't support multiple files in this playground
-- foo.lua
local M = {}
M.cool_function = function() end
return M
-- bar.lua
local foo = require('foo')
foo.cool_function()
--]]
local returns_four_values = function()
return 1, 2, 3, 4
end
first, second, last = returns_four_values()
print("first: ", first)
print("second:", second)
print("last:", last)
-- the \`4\` is discarded :'(
local variable_arguments = function(...)
local arguments = { ... }
for i, v in ipairs({ ... }) do
print(i, v)
end
return table.unpack(arguments)
end
print("===================")
print("1:", variable_arguments("hello", "world", "!"))
print("===================")
print("2:", variable_arguments("hello", "world", "!"), "<lost>")
local single_string = function(s)
return s .. " - WOW!"
end
local x = single_string("hi")
local y = single_string("hi")
print(x, y)
local setup = function(opts)
if opts.default == nil then
opts.default = 17
end
print(opts.default, opts.other)
end
setup({ default = 12, other = false })
setup({ other = true })
local MyTable = {}
function MyTable.something(self, ...) end
function MyTable:something(...) end
local vector_mt = {}
vector_mt.__add = function(left, right)
return setmetatable({
left[1] + right[1],
left[2] + right[2],
left[3] + right[3],
}, vector_mt)
end
local v1 = setmetatable({ 3, 1, 5 }, vector_mt)
local v2 = setmetatable({ -3, 2, 2 }, vector_mt)
local v3 = v1 + v2
print(v3[1], v3[2], v3[3])
print(v3 + v3)
local fib_mt = {
__index = function(self, key)
if key < 2 then return 1 end
-- Update the table, to save the intermediate results
self[key] = self[key - 2] + self[key - 1]
-- Return the result
return self[key]
end
}
local fib = setmetatable({}, fib_mt)
print(fib[5])
print(fib[1000])
`,
};
/* ----- editor ----- */
const editor = new EditorView({
doc: PRESETS.stack,
parent: document.getElementById("editor-host"),
extensions: [
basicSetup,
StreamLanguage.define(luaMode),
catppuccinTheme,
syntaxHighlighting(luaHighlightStyle),
],
});
document.getElementById("preset").onchange = e => {
const text = PRESETS[e.target.value];
editor.dispatch({ changes: { from: 0, to: editor.state.doc.length, insert: text } });
};
/* ----- output helpers ----- */
const out = document.getElementById("out");
const status = document.getElementById("status");
function clearOut() { out.textContent = ""; }
function logLine(s, cls) {
const node = cls ? document.createElement("span") : document.createTextNode(s + "\n");
if (cls) { node.className = cls; node.textContent = s + "\n"; }
out.appendChild(node);
}
/* ----- lua2wasm compiler (in-browser) ----- */
/* The compiler is a freestanding wasm module built with plain clang (no
* Emscripten); lua2wasm-wasm.mjs is the host glue. It needs only env.abort /
* env.log imports, which the glue supplies. */
status.textContent = "loading compiler…";
const compiler = await loadCompiler(
await (await fetch("../build-wasm/lua2wasm.wasm")).arrayBuffer(),
(line) => logLine(line, "err"),
);
/* Tree-shaking is automatic for programs that don't touch _G/_ENV/load/require,
* so a plain compile already prunes everything it safely can. compiler.compile
* throws on a lex/parse/codegen error; the rest of the UI expects the
* "ERROR(...)" text back, so surface the message (it carries that prefix). */
function luaToWat(source) {
try { return compiler.compile(source); }
catch (e) { return e.message; }
}
/* The $names of functions/globals DCE proves dead in this WAT, as a Set.
* Returns an empty Set if the analysis fails (caller just skips dimming). */
function dceDeadNames(wat) {
return new Set(compiler.deadNames(wat) || []);
}
/* ----- wat -> wasm (built-in assembler, no Binaryen) ----- */
function watToWasm(wat) {
return compiler.assemble(wat); /* throws with the assembler message on failure */
}
/* ----- runtime host ----- */
let curInstance;
const helpers = makeHelpers({ getInstance: () => curInstance, formatFloat, cFormatG, cFormatF, cFormatE });
const { luaToString, formatSpec, parseLuaNumber, osDate, osGetenv, objId } = helpers;
/* Lua's os.clock spec asks for "an approximation of CPU time"; in the
* browser the closest cheap monotonic reading is performance.now() since
* the page loaded. We subtract this baseline at every call. */
const clockEpochMs = performance.now();
/* Virtual filesystem backing io.open / io.lines / os.remove etc. We use
* just-bash's pure in-memory FS, created once and reused across Run
* clicks so a file written in one run is readable in the next (it resets
* on page reload). Its API is async, so the fs host imports below bridge
* through JSPI exactly like io.read does. */
const vfs = new InMemoryFs();
const dirOf = (p) => { const i = p.lastIndexOf("/"); return i <= 0 ? "/" : p.slice(0, i); };
/* ----- the run pipeline ----- */
async function run() {
clearOut();
const t0 = performance.now();
const src = editor.state.doc.toString();
let wat;
try {
wat = luaToWat(src);
} catch (e) { logLine("ERROR (compile): " + e, "err"); return; }
if (wat.startsWith("ERROR")) { logLine(wat, "err"); return; }