-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathlower.ts
More file actions
1128 lines (978 loc) · 49.3 KB
/
Copy pathlower.ts
File metadata and controls
1128 lines (978 loc) · 49.3 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
// SPDX-License-Identifier: MIT
/**
* Silicon typed AST → QBE IR text (direct lowering, no WASM IR intermediate)
*
* This pass receives the same type-checked program that the WASM lowerer
* (src/ir/lower.ts) would receive, but produces QBE IR text directly without
* ever constructing IRModule / IRExpr / IRStmt nodes.
*
* Design rationale: the Silicon IR (IRModule) is WASM-shaped — it stores WAT
* instruction strings in BinOp.instr, models globals as WAT globals, uses
* WASM linear memory offsets for data segments, and carries funcref table
* state. None of that applies to a native QBE target. By lowering straight
* to QBE we avoid inheriting those assumptions.
*
* Pipeline position:
* TypedAST (+ ElaboratorRegistry + FunctionSigs)
* ──[this file]──▶ QBE IR text (string)
*
* Story coverage:
* 8-1 — module skeleton: top-level @fn / @extern / @var declarations,
* function signatures, placeholder bodies
* 8-2 — expression lowering: literals, binary ops, local reads/writes
* 8-3 — call lowering: user function calls, @extern, @return
* 8-4 — control flow: if/else as conditional jumps; loop/break/continue
* as basic blocks with labeled jumps
* 8-5 — globals: @var data declarations, load/store for reads/writes
*/
import type { Program } from '../../ast/astNodes'
import type { ElaboratorRegistry } from '../../elaborator/registry'
import type { FunctionSig } from '../../types/typechecker'
import type { SiliconType } from '../../types/types'
import { siliconTypeToQbe, siliconTypeToQbeReturn, lookupOpToQbe } from './types'
import type { QbeType, QbeReturnType } from './types'
// ---------------------------------------------------------------------------
// Lowering context
// ---------------------------------------------------------------------------
/**
* Module-level accumulator. Sections are built up as the program is walked
* and joined into the final QBE IR text by `emitQbeModule`.
*
* Per-function state (temps, labels, local map, instruction lines) lives in
* `QbeFnCtx` and is discarded after each function is emitted.
*/
interface QbeModCtx {
/** Lines of # comments for WASM imports / @extern declarations. */
externs: string[]
/** `data $name = ...` declarations for string literals and other statics. */
dataDecls: string[]
/** `data $name = align N { type value }` for @var globals. */
globalData: string[]
/** Fully-formed QBE function strings ready to be concatenated. */
funcs: string[]
/** Module-level global variable types — for load/store code-gen. */
globalTypes: Map<string, QbeType>
/** Function signatures from the type checker. */
functionSigs: Map<string, FunctionSig>
/** Data-segment label counter (for string literals). */
dataLabelN: number
/** String literal dedup cache: content → label. */
stringCache: Map<string, string>
}
/**
* Per-function emission state. Created fresh for each @fn body; the results
* are flushed into `QbeModCtx.funcs` when the function is complete.
*/
interface QbeFnCtx {
mod: QbeModCtx
/** Instructions for the current basic block, prefixed with `\t`. */
lines: string[]
/** Current block label (written as `@label` at the start of a block). */
currentBlock: string
/** Index into lines[] where the current basic block started (after the label line). */
blockStart: number
/** Monotonically increasing temp counter: %t0, %t1, … */
tempN: number
/** Monotonically increasing label counter: @lbl0, @lbl1, … */
labelN: number
/** Local variable QBE types — parameters + @local declarations. */
locals: Map<string, QbeType>
/** Stack of active loop label pairs — innermost last. */
loopStack: Array<{ exit: string; cont: string }>
/** LIFO stack of deferred cleanup expression emitters (for @defer). */
deferStack: Array<() => void>
/** Return type of the current function. */
returnType: QbeReturnType
}
function freshMod(functionSigs: Map<string, FunctionSig>): QbeModCtx {
return {
externs: [], dataDecls: [], globalData: [], funcs: [],
globalTypes: new Map(), functionSigs,
dataLabelN: 0, stringCache: new Map(),
}
}
function freshFn(mod: QbeModCtx, returnType: QbeReturnType): QbeFnCtx {
return {
mod, lines: [], currentBlock: '@start', blockStart: 0,
tempN: 0, labelN: 0,
locals: new Map(), loopStack: [], deferStack: [],
returnType,
}
}
function freshTemp(fn: QbeFnCtx): string {
return `%t${fn.tempN++}`
}
function freshLabel(fn: QbeFnCtx, prefix = 'lbl'): string {
return `@${prefix}${fn.labelN++}`
}
/** Emit one instruction line into the current function block. */
function emit(fn: QbeFnCtx, line: string): void {
fn.lines.push(`\t${line}`)
}
/** Emit all deferred cleanup expressions in LIFO order (last-in, first-out). */
function emitDeferred(fn: QbeFnCtx): void {
for (let i = fn.deferStack.length - 1; i >= 0; i--) {
fn.deferStack[i]()
}
}
/** True if the current basic block already ends with a terminator instruction. */
function isTerminated(fn: QbeFnCtx): boolean {
for (let i = fn.lines.length - 1; i >= fn.blockStart; i--) {
const t = fn.lines[i].trim()
if (t === '' || t.startsWith('#')) continue
return t.startsWith('jmp') || t.startsWith('ret') || t.startsWith('jnz')
}
return false
}
/** Switch to a new basic block: record the label line then update currentBlock. */
function startBlock(fn: QbeFnCtx, label: string): void {
fn.lines.push(label)
fn.currentBlock = label
fn.blockStart = fn.lines.length // instructions after this index belong to the new block
}
// ---------------------------------------------------------------------------
// Identifier normalisation
// ---------------------------------------------------------------------------
/**
* Map a Silicon identifier to a legal QBE symbol name. QBE function and
* global names are prefixed with `$`; temporaries with `%`. This helper
* produces just the bare name (caller adds the sigil).
*
* Silicon allows names like `my_fn`, `add`, `Vec__push` — all legal in QBE.
* The :: namespace separator is replaced with `__` to flatten it.
*/
function qbeName(name: string): string {
return name.replace(/::/g, '__')
}
/** Unwrap the legacy element/item/statement wrappers the parser emits. */
function unwrap(node: any): any {
if (!node || typeof node !== 'object') return node
if (node.type === 'Element') return unwrap(node.element)
if (node.type === 'Item') return unwrap(node.item)
if (node.type === 'Statement') return unwrap(node.statement)
return node
}
/** Normalise an integer-literal value string for QBE emission: strip `_` digit
* separators and fold any 0x/0b/0o prefix to a decimal value. Returns the
* two's-complement signed-64-bit form so a `u64` literal ≥ 2^63 emits the bit
* pattern QBE's `l` type expects (e.g. max-u64 → "-1"). */
function qbeIntText(raw: any): string {
const s = String(raw ?? '0').replace(/_/g, '')
try { return BigInt.asIntN(64, BigInt(s)).toString() }
catch { return s }
}
/** Strip `_` digit separators from a float-literal value string. */
function qbeFloatText(raw: any): string {
return String(raw ?? 0).replace(/_/g, '')
}
/** If `arg` unwraps to an integer literal, return its QBE-normalised value
* string (full precision, separators/base folded); else undefined. */
function qbeIntLiteralValue(arg: any): string | undefined {
const n = unwrap(arg)
return n && n.type === 'IntLiteral' ? qbeIntText(n.value) : undefined
}
// ---------------------------------------------------------------------------
// Module-level entry point (Story 8-1)
// ---------------------------------------------------------------------------
/**
* Lower a type-checked Silicon program to a QBE IR string.
*
* Walks `program.elements` in two passes:
* 1. Pre-scan: register global names + types so forward references in
* function bodies resolve correctly (mirrors lowerProgram's pre-scan).
* 2. Emit: produce QBE text for each top-level definition.
*
* Returns the concatenated QBE IR text, ready to pipe into `qbe`.
*/
export function lowerToQbe(
program: Program,
_registry: ElaboratorRegistry,
functionSigs: Map<string, FunctionSig>,
): string {
const mod = freshMod(functionSigs)
// Pass 1: register global variable names/types for forward-reference resolution.
for (const el of (program as any).elements as any[]) {
const node = unwrap(el)
if (!node || node.type !== 'Definition') continue
if (node.keyword === '@local' || node.hook === 'global') {
const rawName = node.name?.name ?? node.name ?? ''
const name = qbeName(rawName)
// Type comes from the typechecker's inferredType on the definition node,
// not from functionSigs (which only contains function signatures).
const stype: SiliconType | undefined = node.inferredType as any
mod.globalTypes.set(name, siliconTypeToQbe(stype))
}
}
// Pass 2: emit each top-level definition; collect non-definition statements.
const topLevelStmts: any[] = []
for (const el of (program as any).elements as any[]) {
const node = unwrap(el)
if (!node) continue
if (node.type === 'Definition') {
lowerTopLevel(node, mod)
} else {
topLevelStmts.push(node)
}
}
// Top-level expression statements → $__sgl_entry so injectMainWrapper can
// produce a C-compatible main() that calls it.
if (topLevelStmts.length > 0) {
const fn = freshFn(mod, 'void')
startBlock(fn, '@start')
for (const stmt of topLevelStmts) {
lowerExpr(stmt, fn)
}
if (!isTerminated(fn)) {
emitDeferred(fn)
emit(fn, 'ret')
}
mod.funcs.push(['function $__sgl_entry() {', ...fn.lines, '}'].join('\n'))
}
return emitQbeModule(mod)
}
// ---------------------------------------------------------------------------
// Top-level definition dispatch (Story 8-1)
// ---------------------------------------------------------------------------
function lowerTopLevel(node: any, mod: QbeModCtx): void {
if (!node || node.type !== 'Definition') return
const kw: string = node.keyword ?? ''
const hook: string = node.hook ?? ''
if (kw === '@fn' || hook === 'function') {
lowerFunctionDef(node, mod)
return
}
if (kw === '@extern' || hook === 'extern') {
lowerExternDef(node, mod)
return
}
if (kw === '@local' || hook === 'global') {
lowerGlobalVarDef(node, mod)
return
}
// @type, @struct, @enum, @type_sum — no QBE output at top level;
// their constructors/accessors are emitted as @fn definitions.
}
// ---------------------------------------------------------------------------
// @fn (Story 8-1: signature + placeholder body)
// ---------------------------------------------------------------------------
function lowerFunctionDef(node: any, mod: QbeModCtx): void {
const rawName: string = node.name?.name ?? node.name ?? 'unknown'
const name = qbeName(rawName)
// Use the type-checker's signature as the authoritative source for types.
const sig = mod.functionSigs.get(name)
const returnType: QbeReturnType = sig
? siliconTypeToQbeReturn(sig.result)
: 'void'
// Collect parameter names and their QBE types.
// Prefer sig.params[i] (typechecker-resolved) over AST annotation strings.
const params: Array<{ name: string; qt: QbeType }> = []
if (node.params) {
for (let i = 0; i < (node.params as any[]).length; i++) {
const pNode = unwrap((node.params as any[])[i])
const pName = qbeName(pNode?.name?.name ?? pNode?.name ?? `p${i}`)
const sigType: SiliconType | undefined = sig?.params[i]
const pType = pNode?.typeAnnotation?.typeName ?? pNode?.inferredType
const astType: SiliconType | undefined =
typeof pType === 'string'
? resolveTypeName(pType)
: (pType as SiliconType | undefined)
params.push({ name: pName, qt: siliconTypeToQbe(sigType ?? astType) })
}
}
const fn = freshFn(mod, returnType)
// Register params as locals so body emission (8-2+) can type them.
for (const { name: pn, qt } of params) {
fn.locals.set(pn, qt)
}
// Build the function header line.
const paramStr = params.map(p => `${p.qt} %${p.name}`).join(', ')
const retPrefix = returnType !== 'void' ? `${returnType} ` : ''
// `main` must be exported so the C runtime (_start in crt1.o) can find it.
const exportPrefix = name === 'main' ? 'export ' : ''
const header = `${exportPrefix}function ${retPrefix}$${name}(${paramStr}) {`
// Emit the entry block — startBlock records the blockStart index.
startBlock(fn, '@start')
// Lower the body expression if present; placeholder if not.
// AST layout: node.binding.expression holds the body (after ':=').
const bodyExpr = node.binding?.expression ?? node.body ?? null
if (bodyExpr) {
const trailing = lowerExpr(bodyExpr, fn)
if (!isTerminated(fn)) {
emitDeferred(fn)
if (returnType !== 'void' && trailing) {
emit(fn, `ret ${trailing}`)
} else {
emit(fn, 'ret')
}
}
} else {
emit(fn, returnType !== 'void' ? `ret 0` : 'ret')
}
const funcText = [header, ...fn.lines, '}'].join('\n')
mod.funcs.push(funcText)
}
// ---------------------------------------------------------------------------
// @extern (Story 8-1)
// ---------------------------------------------------------------------------
function lowerExternDef(node: any, mod: QbeModCtx): void {
const rawName: string = node.name?.name ?? node.name ?? 'unknown'
const name = qbeName(rawName)
const sig = mod.functionSigs.get(name)
const params = (sig?.params ?? []).map((p, i) => `${siliconTypeToQbe(p)} %p${i}`).join(', ')
const retPrefix = sig ? `${siliconTypeToQbeReturn(sig.result)} ` : ''
// QBE has no import declaration — extern functions are resolved by the
// linker. Emit as a comment so the QBE source is self-documenting.
mod.externs.push(`# extern function ${retPrefix}$${name}(${params})`)
}
// ---------------------------------------------------------------------------
// @var global (Story 8-1 / 8-5)
// ---------------------------------------------------------------------------
function lowerGlobalVarDef(node: any, mod: QbeModCtx): void {
const rawName: string = node.name?.name ?? node.name ?? 'unknown'
const name = qbeName(rawName)
const qt = mod.globalTypes.get(name) ?? 'w'
const align = qt === 'l' ? 8 : 4
// Evaluate constant initializer. Only literal values are supported here;
// complex expressions would require a constructor function (future story).
const initExpr = node.binding?.expression
const initVal = constInitVal(initExpr, qt)
mod.globalData.push(`data $${name} = align ${align} { ${qt} ${initVal} }`)
}
/** Extract a QBE-literal init value from a constant expression, or 0. */
function constInitVal(expr: any, qt: QbeType): string {
if (!expr) return '0'
const n = unwrap(expr)
if (!n) return '0'
switch (n.type) {
case 'IntLiteral': return qbeIntText(n.value)
case 'FloatLiteral': return qt === 's' ? `s_${qbeFloatText(n.value)}` : qbeFloatText(n.value)
case 'BooleanLiteral': return n.value === true ? '1' : '0'
default: return '0'
}
}
// ---------------------------------------------------------------------------
// Expression lowering (Story 8-1: stubs; 8-2+ fills in)
// ---------------------------------------------------------------------------
/**
* Lower a typed AST expression node into the current function context.
* Returns the QBE value string for the expression's result:
* - A literal integer/float: `42`, `s_3.14`
* - A local/parameter reference: `%name`
* - A fresh temporary that holds the result: `%t0`
* - An empty string for void/no-value expressions
*
* Story 8-1 provides the dispatch skeleton with stubs; stories 8-2 through
* 8-4 fill in each case.
*/
function lowerExpr(node: any, fn: QbeFnCtx): string {
if (!node) return ''
const n = unwrap(node)
if (!n) return ''
switch (n.type) {
// -- Literals --------------------------------------------------------
case 'IntLiteral':
return qbeIntText(n.value)
case 'FloatLiteral':
return `s_${qbeFloatText(n.value)}`
case 'BooleanLiteral':
return n.value === true || n.value === 'true' ? '1' : '0'
// -- Identifier / variable reference ---------------------------------
// Parser wraps variable refs in a Namespace node with a path array.
case 'Namespace': {
const varName = qbeName((n.path as string[])?.[0] ?? n.name ?? '')
if (fn.locals.has(varName)) return `%${varName}`
if (fn.mod.globalTypes.has(varName)) {
const qt = fn.mod.globalTypes.get(varName)!
const tmp = freshTemp(fn)
emit(fn, `${tmp} =${qt} load${qt} $${varName}`)
return tmp
}
// Probably a zero-arg function call
return lowerCall(varName, [], fn)
}
case 'Identifier': {
const varName = qbeName(n.name ?? '')
if (fn.locals.has(varName)) return `%${varName}`
if (fn.mod.globalTypes.has(varName)) {
const qt = fn.mod.globalTypes.get(varName)!
const tmp = freshTemp(fn)
emit(fn, `${tmp} =${qt} load${qt} $${varName}`)
return tmp
}
return lowerCall(n.name ?? '', [], fn)
}
// -- Binary operators ------------------------------------------------
case 'BinaryOp':
return lowerBinaryOp(n, fn)
// -- Function calls --------------------------------------------------
case 'FunctionCall':
return lowerFunctionCall(n, fn)
// -- Block -----------------------------------------------------------
case 'Block': {
const stmts: any[] = n.items ?? n.statements ?? n.stmts ?? []
for (const s of stmts) lowerExpr(s, fn)
const trail = n.trailing ?? n.expression ?? n.result
return trail ? lowerExpr(trail, fn) : ''
}
// -- Assignment (x = val) -------------------------------------------
// Parser produces target as a Namespace node, not a plain name field.
case 'Assignment': {
const targetNode = n.target ?? n.name
const varName = qbeName(
Array.isArray(targetNode?.path)
? (targetNode.path as string[])[0] ?? ''
: targetNode?.name ?? targetNode ?? ''
)
const val = lowerExpr(n.value, fn)
const qt = fn.locals.get(varName) ?? fn.mod.globalTypes.get(varName) ?? 'w'
if (fn.mod.globalTypes.has(varName)) {
emit(fn, `store${qt} ${val}, $${varName}`)
} else {
// Registers local on first assignment if not already declared.
fn.locals.set(varName, qt)
emit(fn, `%${varName} =${qt} copy ${val}`)
}
return ''
}
// -- Definition inside a function body (@local, etc.) ----------------
case 'Definition':
return lowerLocalDef(n, fn)
// -- String literals -------------------------------------------------
case 'StringLiteral': {
const text: string = n.value ?? ''
if (fn.mod.stringCache.has(text)) {
return `$${fn.mod.stringCache.get(text)}`
}
const label = `str${fn.mod.dataLabelN++}`
fn.mod.stringCache.set(text, label)
// Native backend: plain C string (NUL-terminated, no length prefix).
// The Silicon length-prefix layout is a WASM artifact; in native code
// $label points directly at the first byte, compatible with libc functions.
const escaped = text.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t')
fn.mod.dataDecls.push(`data $${label} = align 1 { b "${escaped}", b 0 }`)
return `$${label}`
}
case 'ArrayLiteral':
// `$[…]` needs the length-prefixed heap layout (alloc_array +
// arr_load/arr_store), and QBE has no allocator surface yet —
// same gap as @with_arena above. Reject loudly instead of
// silently lowering to 0.
throw new Error(
`[QBE lower] '$[…]' array literals are not yet supported on the native ` +
`backend (QBE allocator wiring is a follow-up story; see the @with_arena ` +
`note in this file). Build with the default (WASM) backend.`,
)
// -- Stubs for future node types -------------------------------------
default:
return '0'
}
}
// ---------------------------------------------------------------------------
// Binary operator lowering (Story 8-2)
// ---------------------------------------------------------------------------
function lowerBinaryOp(node: any, fn: QbeFnCtx): string {
const op: string = node.operator ?? ''
// Assignment form `x = val` parsed as BinaryOp in expression tail position
if (op === '=') {
const varName = qbeName(
node.left?.path?.[0] ?? node.left?.name ?? node.left?.identifier?.name ?? ''
)
const val = lowerExpr(node.right, fn)
const qt = fn.locals.get(varName) ?? fn.mod.globalTypes.get(varName) ?? 'w'
if (fn.mod.globalTypes.has(varName)) {
emit(fn, `store${qt} ${val}, $${varName}`)
} else {
emit(fn, `%${varName} =${qt} copy ${val}`)
}
return ''
}
// Short-circuit OR: `a || b` ≡ `if a then 1 else b`. Mirrors the WASM
// strata lowering (logic.si: IRIf(left, 1, right)); must NOT be treated as
// a plain binop — the right operand is only evaluated when left is falsy.
if (op === '||') {
return lowerShortCircuit(fn, node.left, () => '1', () => lowerExpr(node.right, fn))
}
const left = lowerExpr(node.left, fn)
const right = lowerExpr(node.right, fn)
// Determine the operand type kind for instruction selection.
// Strata 2.0 operators use on::lower handlers — the registry doesn't store
// WAT intrinsic strings. We resolve directly via lookupOpToQbe, mirroring
// exactly what the WASM strata emit per (operator, operand type):
// - `| ^ <<` are signedness-agnostic and always 32-bit (bitwise.si emits
// i32.* unconditionally — even for Int64), so force the 'Int' table.
// - `>>` follows the operand: unsigned → logical shr (i32/i64.shr_u);
// signed Int *and* Int64 → arithmetic 32-bit sar (bitwise.si i32.shr_s;
// there is no `>>:Int64` overload), so map Int64 `>>` to 'Int'.
// - everything else routes by the operand's own type, including the
// unsigned div/rem/comparison overloads (UInt8/16/32 → 'UInt32' bucket).
const leftType: SiliconType | undefined = node.left?.inferredType as any
const k = leftType?.kind
const isUnsigned32 = k === 'UInt8' || k === 'UInt16' || k === 'UInt32'
const typeKind =
(op === '|' || op === '^' || op === '<<') ? 'Int'
: op === '>>' ? (k === 'UInt64' ? 'UInt64' : isUnsigned32 ? 'UInt32' : 'Int')
: k === 'Int64' ? 'Int64'
: k === 'UInt64' ? 'UInt64'
: k === 'Float' ? 'Float'
: isUnsigned32 ? 'UInt32'
: 'Int'
const entry = lookupOpToQbe(op, typeKind)
if (entry) {
const tmp = freshTemp(fn)
emit(fn, `${tmp} =${entry.qt} ${entry.instr} ${left}, ${right}`)
return tmp
}
emit(fn, `# TODO(qbe): operator '${op}' (typeKind=${typeKind})`)
return '0'
}
// ---------------------------------------------------------------------------
// Function call lowering (Story 8-3)
// ---------------------------------------------------------------------------
function lowerFunctionCall(node: any, fn: QbeFnCtx): string {
// Callee is a Namespace node { path: ['WASM', 'i32_store'] } or a plain string.
// Join all path segments so WASM::i32_store → 'WASM__i32_store', not just 'WASM'.
const nameNode = node.name ?? node.callee
const callee: string =
Array.isArray(nameNode?.path)
? qbeName((nameNode.path as string[]).join('::'))
: qbeName(typeof nameNode === 'string' ? nameNode : (nameNode?.name ?? ''))
const args: any[] = node.args ?? node.arguments ?? []
// Builtin keywords that map to QBE control instructions.
if (node.isBuiltin) return lowerBuiltinCall(callee, args, fn)
// WASM:: namespace intrinsics — map to QBE memory/arithmetic instructions.
if (callee.startsWith('WASM__')) return lowerWasmIntrinsic(callee, args, fn)
return lowerCall(callee, args, fn, node)
}
function lowerBuiltinCall(callee: string, args: any[], fn: QbeFnCtx): string {
switch (callee) {
case '@return': {
const val = args.length > 0 ? lowerExpr(args[0], fn) : ''
emitDeferred(fn)
if (val) emit(fn, `ret ${val}`)
else emit(fn, 'ret')
return ''
}
case '@defer': {
// Capture the deferred expression; run it LIFO at every ret site.
const deferredExpr = args[0]
fn.deferStack.push(() => { lowerExpr(deferredExpr, fn) })
return ''
}
case '@if':
return lowerIf(args, fn)
case '@loop':
return lowerLoop(args, fn)
case '@break': {
const top = fn.loopStack[fn.loopStack.length - 1]
if (top) emit(fn, `jmp ${top.exit}`)
else emit(fn, '# @break outside loop')
return ''
}
case '@continue': {
const top = fn.loopStack[fn.loopStack.length - 1]
if (top) emit(fn, `jmp ${top.cont}`)
else emit(fn, '# @continue outside loop')
return ''
}
// -- Boolean literals arriving as builtins (stratum path) ------------
case '@true': return '1'
case '@false': return '0'
// -- Short-circuit logic keywords (logic.si) -------------------------
// Mirror the WASM strata lowering, which is IRIf-based:
// @and(a, b) ≡ if a then b else 0 @or(a, b) ≡ if a then 1 else b
// @not(x) ≡ x == 0
case '@and':
return lowerShortCircuit(fn, args[0], () => lowerExpr(args[1], fn), () => '0')
case '@or':
return lowerShortCircuit(fn, args[0], () => '1', () => lowerExpr(args[1], fn))
case '@not': {
const x = args[0] ? lowerExpr(args[0], fn) : '0'
const tmp = freshTemp(fn)
emit(fn, `${tmp} =w ceqw ${x}, 0`)
return tmp
}
// -- Phase 9c — explicit arenas not yet supported on QBE ─────────────
// The WASM backend's bump allocator lives in the prelude IR; QBE
// doesn't yet have an allocator surface (`heap`, `mem_copy`,
// `arena_promote`, …). Wiring those up requires either a libc
// bridge (malloc/free) or a fresh bump allocator emitted as QBE
// data + functions — outside Phase 9c's scope. Reject with a
// structured error pointing to the follow-up story so users see
// why their program can't `--native` compile yet.
case '@with_arena':
case '@move_to_parent_arena':
throw new Error(
`[QBE lower] '${callee}' is not yet supported on the native backend. ` +
`Phase 9c shipped @with_arena / @move_to_parent_arena for the WASM ` +
`backend only; QBE allocator wiring is a follow-up story (tracked under ` +
`Phase 9c-6 in docs/v1-user-stories.html). Build with the default ` +
`(WASM) backend, or omit the arena strata for --native builds.`,
)
// -- Type casts -------------------------------------------------------
case '@toFloat': {
// Int → Float (signed word → single-precision float)
const arg = args[0] ? lowerExpr(args[0], fn) : '0'
const argType: SiliconType | undefined = args[0]?.inferredType as any
const tmp = freshTemp(fn)
if (argType?.kind === 'Int64') {
emit(fn, `${tmp} =s sltof ${arg}`) // signed long → float
} else {
emit(fn, `${tmp} =s swtof ${arg}`) // signed word → float
}
return tmp
}
case '@toInt': {
const arg = args[0] ? lowerExpr(args[0], fn) : '0'
const argType: SiliconType | undefined = args[0]?.inferredType as any
const tmp = freshTemp(fn)
if (argType?.kind === 'Int64') {
// Int64 → Int: truncate long to word
emit(fn, `${tmp} =w copy ${arg}`)
} else {
// Float → Int: single-precision float → signed word
emit(fn, `${tmp} =w stosi ${arg}`)
}
return tmp
}
case '@toInt64':
case '@i64': {
// Int → Int64. A literal folds to a full-precision `l` constant —
// `extsw` would truncate it through 32 bits (the original bug); a
// runtime word sign-extends.
const lit = qbeIntLiteralValue(args[0])
const tmp = freshTemp(fn)
if (lit !== undefined) { emit(fn, `${tmp} =l copy ${lit}`); return tmp }
const arg = args[0] ? lowerExpr(args[0], fn) : '0'
emit(fn, `${tmp} =l extsw ${arg}`)
return tmp
}
case '@toU64':
case '@u64': {
// Int → UInt64. A literal folds; an already-64-bit value is a pure
// relabel; a runtime word zero-extends.
const lit = qbeIntLiteralValue(args[0])
const tmp = freshTemp(fn)
if (lit !== undefined) { emit(fn, `${tmp} =l copy ${lit}`); return tmp }
const arg = args[0] ? lowerExpr(args[0], fn) : '0'
const argType: SiliconType | undefined = args[0]?.inferredType as any
if (argType?.kind === 'Int64') {
emit(fn, `${tmp} =l copy ${arg}`) // already 64-bit: relabel
} else {
emit(fn, `${tmp} =l extuw ${arg}`) // word → zero-extend
}
return tmp
}
default:
return ''
}
}
// ---------------------------------------------------------------------------
// WASM:: intrinsic → QBE instruction mapping
//
// Silicon code uses `&WASM::i32_store`, `&WASM::i32_load`, etc. as direct
// wrappers around WASM instructions. When targeting native via QBE we
// replace each with the equivalent QBE memory/arithmetic/cast instruction.
//
// Argument convention matches the Silicon stratum definitions:
// store ops: (addr, val) → QBE `store<t> val, addr`
// load ops: (addr) → QBE `%t =<t> load<t> addr`
// binary ops: (a, b) → QBE `%t =<t> <instr> a, b`
// cast ops: (a) → QBE `%t =<t> <instr> a`
// ---------------------------------------------------------------------------
function lowerWasmIntrinsic(callee: string, args: any[], fn: QbeFnCtx): string {
const a = args.map(x => lowerExpr(x, fn))
switch (callee) {
// -- Memory: stores --------------------------------------------------
case 'WASM__i32_store': { emit(fn, `storew ${a[1]}, ${a[0]}`); return '' }
case 'WASM__i32_store8': { emit(fn, `storeb ${a[1]}, ${a[0]}`); return '' }
case 'WASM__i64_store': { emit(fn, `storel ${a[1]}, ${a[0]}`); return '' }
case 'WASM__f32_store': { emit(fn, `stores ${a[1]}, ${a[0]}`); return '' }
// -- Memory: loads ---------------------------------------------------
case 'WASM__i32_load': { const t = freshTemp(fn); emit(fn, `${t} =w loadw ${a[0]}`); return t }
case 'WASM__i32_load8_u': { const t = freshTemp(fn); emit(fn, `${t} =w loadub ${a[0]}`); return t }
case 'WASM__i32_load8_s': { const t = freshTemp(fn); emit(fn, `${t} =w loadsb ${a[0]}`); return t }
case 'WASM__i32_load16_u':{ const t = freshTemp(fn); emit(fn, `${t} =w loaduh ${a[0]}`); return t }
case 'WASM__i32_load16_s':{ const t = freshTemp(fn); emit(fn, `${t} =w loadsh ${a[0]}`); return t }
case 'WASM__i64_load': { const t = freshTemp(fn); emit(fn, `${t} =l loadl ${a[0]}`); return t }
case 'WASM__f32_load': { const t = freshTemp(fn); emit(fn, `${t} =s loads ${a[0]}`); return t }
// -- i32 arithmetic --------------------------------------------------
case 'WASM__i32_add': { const t = freshTemp(fn); emit(fn, `${t} =w add ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_sub': { const t = freshTemp(fn); emit(fn, `${t} =w sub ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_mul': { const t = freshTemp(fn); emit(fn, `${t} =w mul ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_div_s': { const t = freshTemp(fn); emit(fn, `${t} =w div ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_rem_s': { const t = freshTemp(fn); emit(fn, `${t} =w rem ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_and': { const t = freshTemp(fn); emit(fn, `${t} =w and ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_or': { const t = freshTemp(fn); emit(fn, `${t} =w or ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_xor': { const t = freshTemp(fn); emit(fn, `${t} =w xor ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_shl': { const t = freshTemp(fn); emit(fn, `${t} =w shl ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_shr_s': { const t = freshTemp(fn); emit(fn, `${t} =w sar ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_shr_u': { const t = freshTemp(fn); emit(fn, `${t} =w shr ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_eqz': { const t = freshTemp(fn); emit(fn, `${t} =w ceqw ${a[0]}, 0`); return t }
// -- i64 arithmetic --------------------------------------------------
case 'WASM__i64_add': { const t = freshTemp(fn); emit(fn, `${t} =l add ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_sub': { const t = freshTemp(fn); emit(fn, `${t} =l sub ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_mul': { const t = freshTemp(fn); emit(fn, `${t} =l mul ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_div_s': { const t = freshTemp(fn); emit(fn, `${t} =l div ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_rem_s': { const t = freshTemp(fn); emit(fn, `${t} =l rem ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_and': { const t = freshTemp(fn); emit(fn, `${t} =l and ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_or': { const t = freshTemp(fn); emit(fn, `${t} =l or ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_xor': { const t = freshTemp(fn); emit(fn, `${t} =l xor ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_shl': { const t = freshTemp(fn); emit(fn, `${t} =l shl ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_shr_s': { const t = freshTemp(fn); emit(fn, `${t} =l sar ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_shr_u': { const t = freshTemp(fn); emit(fn, `${t} =l shr ${a[0]}, ${a[1]}`); return t }
// -- f32 arithmetic --------------------------------------------------
case 'WASM__f32_add': { const t = freshTemp(fn); emit(fn, `${t} =s add ${a[0]}, ${a[1]}`); return t }
case 'WASM__f32_sub': { const t = freshTemp(fn); emit(fn, `${t} =s sub ${a[0]}, ${a[1]}`); return t }
case 'WASM__f32_mul': { const t = freshTemp(fn); emit(fn, `${t} =s mul ${a[0]}, ${a[1]}`); return t }
case 'WASM__f32_div': { const t = freshTemp(fn); emit(fn, `${t} =s div ${a[0]}, ${a[1]}`); return t }
case 'WASM__f32_neg': { const t = freshTemp(fn); emit(fn, `${t} =s neg ${a[0]}`); return t }
case 'WASM__f32_sqrt': {
// QBE has no sqrt instruction — call the C runtime sqrtf.
const t = freshTemp(fn)
emit(fn, `${t} =s call $sqrtf(s ${a[0]})`)
return t
}
// -- i32 comparisons -------------------------------------------------
case 'WASM__i32_eq': { const t = freshTemp(fn); emit(fn, `${t} =w ceqw ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_ne': { const t = freshTemp(fn); emit(fn, `${t} =w cnew ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_lt_s': { const t = freshTemp(fn); emit(fn, `${t} =w csltw ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_le_s': { const t = freshTemp(fn); emit(fn, `${t} =w cslew ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_gt_s': { const t = freshTemp(fn); emit(fn, `${t} =w csgtw ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_ge_s': { const t = freshTemp(fn); emit(fn, `${t} =w csgew ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_lt_u': { const t = freshTemp(fn); emit(fn, `${t} =w cultw ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_le_u': { const t = freshTemp(fn); emit(fn, `${t} =w culew ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_gt_u': { const t = freshTemp(fn); emit(fn, `${t} =w cugtw ${a[0]}, ${a[1]}`); return t }
case 'WASM__i32_ge_u': { const t = freshTemp(fn); emit(fn, `${t} =w cugew ${a[0]}, ${a[1]}`); return t }
// -- i64 comparisons -------------------------------------------------
case 'WASM__i64_eq': { const t = freshTemp(fn); emit(fn, `${t} =w ceql ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_ne': { const t = freshTemp(fn); emit(fn, `${t} =w cnel ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_lt_s': { const t = freshTemp(fn); emit(fn, `${t} =w csltl ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_le_s': { const t = freshTemp(fn); emit(fn, `${t} =w cslel ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_gt_s': { const t = freshTemp(fn); emit(fn, `${t} =w csgtl ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_ge_s': { const t = freshTemp(fn); emit(fn, `${t} =w csgel ${a[0]}, ${a[1]}`); return t }
case 'WASM__i64_lt_u': { const t = freshTemp(fn); emit(fn, `${t} =w cultl ${a[0]}, ${a[1]}`); return t }
// -- f32 comparisons -------------------------------------------------
case 'WASM__f32_eq': { const t = freshTemp(fn); emit(fn, `${t} =w ceqs ${a[0]}, ${a[1]}`); return t }
case 'WASM__f32_ne': { const t = freshTemp(fn); emit(fn, `${t} =w cnes ${a[0]}, ${a[1]}`); return t }
case 'WASM__f32_lt': { const t = freshTemp(fn); emit(fn, `${t} =w clts ${a[0]}, ${a[1]}`); return t }
case 'WASM__f32_le': { const t = freshTemp(fn); emit(fn, `${t} =w cles ${a[0]}, ${a[1]}`); return t }
case 'WASM__f32_gt': { const t = freshTemp(fn); emit(fn, `${t} =w cgts ${a[0]}, ${a[1]}`); return t }
case 'WASM__f32_ge': { const t = freshTemp(fn); emit(fn, `${t} =w cges ${a[0]}, ${a[1]}`); return t }
// -- Casts -----------------------------------------------------------
case 'WASM__i32_wrap_i64': { const t = freshTemp(fn); emit(fn, `${t} =w copy ${a[0]}`); return t }
case 'WASM__i64_extend_i32_s': { const t = freshTemp(fn); emit(fn, `${t} =l extsw ${a[0]}`); return t }
case 'WASM__i64_extend_i32_u': { const t = freshTemp(fn); emit(fn, `${t} =l extuw ${a[0]}`); return t }
case 'WASM__f32_convert_i32_s': { const t = freshTemp(fn); emit(fn, `${t} =s swtof ${a[0]}`); return t }
case 'WASM__f32_convert_i32_u': { const t = freshTemp(fn); emit(fn, `${t} =s uwtof ${a[0]}`); return t }
case 'WASM__f32_convert_i64_s': { const t = freshTemp(fn); emit(fn, `${t} =s sltof ${a[0]}`); return t }
case 'WASM__i32_trunc_f32_s': { const t = freshTemp(fn); emit(fn, `${t} =w stosi ${a[0]}`); return t }
case 'WASM__i32_trunc_f32_u': { const t = freshTemp(fn); emit(fn, `${t} =w stoui ${a[0]}`); return t }
case 'WASM__i64_trunc_f32_s': { const t = freshTemp(fn); emit(fn, `${t} =l dtosi ${a[0]}`); return t }
case 'WASM__f32_demote_f64': { const t = freshTemp(fn); emit(fn, `${t} =s truncd ${a[0]}`); return t }
case 'WASM__f64_promote_f32': { const t = freshTemp(fn); emit(fn, `${t} =d exts ${a[0]}`); return t }
// -- Special ---------------------------------------------------------
case 'WASM__i32_clz': {
// QBE has no clz — forward to libc __builtin_clz equivalent
const t = freshTemp(fn)
emit(fn, `${t} =w call $__builtin_clz(w ${a[0]})`)
return t
}
default:
emit(fn, `# TODO: WASM intrinsic '${callee}' not mapped to QBE`)
return '0'
}
}
function lowerCall(callee: string, args: any[], fn: QbeFnCtx, callNode?: any): string {
const argVals = args.map(a => lowerExpr(a, fn))
// Build typed argument list for QBE: `w %x`, `s s_1.0`, …
const typedArgs = argVals.map((v, i) => {
const argType: SiliconType | undefined = args[i]?.inferredType as any
return `${siliconTypeToQbe(argType)} ${v}`
}).join(', ')
// Return type: prefer the typechecker's function sig, fall back to the
// call site's inferred type (populated by the typechecker for all calls).
const sig = fn.mod.functionSigs.get(callee)
const callSiteType: SiliconType | undefined = callNode?.inferredType as any
const retType: QbeReturnType = sig
? siliconTypeToQbeReturn(sig.result)
: siliconTypeToQbeReturn(callSiteType)
if (retType === 'void') {
emit(fn, `call $${callee}(${typedArgs})`)
return ''
}
const tmp = freshTemp(fn)
emit(fn, `${tmp} =${retType} call $${callee}(${typedArgs})`)
// C `bool` returns set only the low byte of the return register (`al`); the
// upper bits are unspecified. Silicon would otherwise read the full 32-bit
// word as a signed Int — e.g. raylib's `IsKeyDown` leaves garbage above the
// bool byte, so every call tested as "truthy". Mask `Bool` results to the
// low byte so the value is an exact 0/1.
const resultType: SiliconType | undefined = (sig?.result ?? callSiteType) as any
if (resultType?.kind === 'Bool' && retType === 'w') {
const masked = freshTemp(fn)
emit(fn, `${masked} =w and ${tmp}, 255`)
return masked
}
return tmp
}
// ---------------------------------------------------------------------------
// @if lowering (Story 8-4)
//
// args[0] = condition expr
// args[1] = then-Block
// args[2] = else-Block (optional)
//
// Value-returning @if (used in expression position) emits phi at merge.
// Statement-level @if (result discarded) avoids the phi overhead.
// ---------------------------------------------------------------------------
function lowerIf(args: any[], fn: QbeFnCtx): string {
const cond = args[0]
const thenBlock = args[1]
const elseBlock = args[2]
const hasElse = !!elseBlock
const condVal = lowerExpr(cond, fn)
const thenLabel = freshLabel(fn, 'then')
const elseLabel = hasElse ? freshLabel(fn, 'else') : ''
const mergeLabel = freshLabel(fn, 'end')
emit(fn, `jnz ${condVal}, ${thenLabel}, ${hasElse ? elseLabel : mergeLabel}`)
// Then arm
startBlock(fn, thenLabel)
const thenVal = lowerExpr(thenBlock, fn)
const thenExit = fn.currentBlock
if (!isTerminated(fn)) emit(fn, `jmp ${mergeLabel}`)
// Else arm (if present)
let elseVal = ''
let elseExit = ''
if (hasElse) {
startBlock(fn, elseLabel)
elseVal = lowerExpr(elseBlock, fn)
elseExit = fn.currentBlock
if (!isTerminated(fn)) emit(fn, `jmp ${mergeLabel}`)
}
startBlock(fn, mergeLabel)
// Emit phi if both arms produced a value and we're in expression position.
if (hasElse && thenVal && elseVal && thenVal !== '' && elseVal !== '') {
const resultType: SiliconType | undefined = thenBlock?.inferredType as any
const qt = siliconTypeToQbe(resultType)
const tmp = freshTemp(fn)
emit(fn, `${tmp} =${qt} phi ${thenExit} ${thenVal}, ${elseExit} ${elseVal}`)
return tmp
}
return ''
}
/**
* Emit a short-circuit conditional value `if cond then <thenVal> else <elseVal>`,
* mirroring lowerIf's jnz + phi shape. Used by the logic operators ||, @and, @or
* whose strata lowering (logic.si) is IRIf-based: the non-taken branch's value
* thunk is never evaluated, so a side-effecting operand is correctly skipped.
* Result is an i32 ('w') — the type of all v1.0 logic operands.