do not edit — generated by btf.
git.druid.rocksindexdruid520nsccsrc/jlower5.pl

src/jlower5.pl


#!/usr/bin/env perl
# jlower5 -- the nscc lowering stage, sixth stage of the nscc pipeline.
# owns: flattening nested expressions into flat three-address code,
# allocating a fresh %tN tmp for every intermediate value, making every
# load/store explicit (a ref to a local is a load, an assign to a local
# is a store, a deref is an 8-byte ld8/st8 through the address -- nsc
# ptrs are untyped, see jtype3), assigning every local a stack slot
# (%vN = slot T -8(%rbp), -16(%rbp), ... in decl order) and every
# global its sym id. labels/gotos from jdesugar4 pass straight through.
#
# output format .lir.j5: one instruction per line, 5-space indent, an
# explicit %tN tmp for every value and %vN slot for every local:
#
#   func NAME sym=N linkage=LOCAL|GLOBAL ret=T frame=F locals=K
#        %v0 = slot T -8(%rbp)
#        %t0 = const T VAL
#        %t0 = str "..."           (rodata string; numbered .LC0... in order)
#        %t0 = load T %vN          |  %t0 = load T sym=N name=X
#        st T %tN, %vN             |  st T %tN, sym=N name=X
#        %t0 = ld8 %tN             |  st8 %tN, %tM        (deref, 8 bytes)
#        %t0 = add T %tA, %tB      (sub mul div mod and or xor)
#        %t0 = shl T %tA, %tB      |  %t0 = sar T %tA, %tB
#        %t0 = neg T %tA           |  %t0 = not T %tA
#        %t0 = cast T %tA
#        %t0 = cmp T eq|ne|lt|le|gt|ge %tA, %tB
#        %t0 = addrof %vN          |  %t0 = addrof sym=N name=X
#        %t0 = call T sym=N name=X (T %tA) (T %tB) ...  (T=void: no %t0)
#        %t0 = icall T %tF (T %tA) (T %tB) ...  (indirect, through ptr %tF)
#        ret T %tN                 |  ret void
#        if %tN L                  |  goto L         |  label L
#   end
#   gvar NAME sym=N linkage=LOCAL|GLOBAL T VAL
#
# every operand type in the lir comes from the ': T' atoms jtype3
# attached (decls and casts carry their own); jlower5 never re-derives
# or trusts any implicit width -- if an annotation is missing, that is
# an error. global loads/stores/addrof carry the symbol's name= as
# well as its id, since later stages must spell it in the asm and have
# no symbol table of their own.
#
# this stage's own mini pipeline: in -- preproc -- proc -- out, where
# preproc is the cast.j4 deserialization and proc is the lowering.
use v5.16;
use strict;
use warnings FATAL => 'all';
use FindBin;
use lib "$FindBin::Bin";
use jcplib::util qw(stage_err);
use jcplib::tree qw(read_tree find_kid);
 
my $RAW = do { local $/; <STDIN>; };
my @OUT;        # lir lines
my $TMP_N = 0;  # next tmp number
my $LABEL_N = 0; # next label number
my $GVARS = ''; # gvar lines, emitted after all funcs
my %STRMAP;     # literal content -> .LCn (identical literals share one blob)
my $STR_N = 0;  # next string number
 
# symid -> {type => .., kind => 'local'|'gvar', slot => '%vN'|undef, slotno => .., nwords => .., name => ..}
my %SYMS;
 
# struct name -> field count (from the unit's struct nodes: every
# field is one word).
my %STRUCTS;
 
my $IND = '     ';
 
sub new_tmp { return '%t' . $TMP_N++; }
 
sub err_ctx {
	my ($msg) = @_;
	stage_err("jlower5: $msg");
}
 
sub type_of {
	my ($node) = @_;
	my $atoms = $node->{atoms};
	return $atoms->[-1] if @$atoms && $atoms->[-2] && $atoms->[-2] eq ':';
	err_ctx("missing type annotation on node '$node->{op}'");
}
 
sub symid_of {
	my ($node) = @_;
	# sym= ids are non-negative for real symbols, negative for
	# jdesugar4-created tmps -- both are just ids downstream.
	my ($s) = map { /^sym=(-?\d+)$/ ? $1 : () } @{$node->{atoms}};
	return $s if defined $s;
	# (ref N) nodes carry the bare id as their only atom.
	my ($b) = grep { /^-?\d+$/ } @{$node->{atoms}};
	err_ctx("missing sym= on node '$node->{op}'") unless defined $b;
	return $b;
}
 
sub name_of {
	my ($node) = @_;
	my ($n) = map { /^name=(.+)$/ ? $1 : () } @{$node->{atoms}};
	return $n;
}
 
sub emit {
	my ($line) = @_;
	push @OUT, $IND . $line;
	return;
}
 
# the word count of a type, in 8-byte storage slots: one per scalar,
# one per struct field, and ceil(N*width/8) for an array (arrays are
# byte-packed, so an i8[3] takes one slot).
sub words_of {
	my ($t) = @_;
	if($t =~ /^struct\.(.+)$/) { return scalar @{$STRUCTS{$1}}; }
	if($t =~ /^\[(\d+)\](.+)$/) { my $b = $1 * byte_width($2); return ($b + 7) >> 3; }
	return 1;
}
 
# the byte width of a scalar/ptr type (arrays of structs are rejected
# earlier, so the struct case never arises here).
sub byte_width {
	my ($t) = @_;
	return 8 if $t eq 'i64' || $t eq 'u64' || $t eq 'ptr';
	return 4 if $t eq 'i32' || $t eq 'u32';
	return 2 if $t eq 'i16' || $t eq 'u16';
	return 1 if $t eq 'i8' || $t eq 'u8';
	err_ctx("no byte width for type '$t'");
}
 
 
sub flat_expr {
	my ($e) = @_;
	my $op = $e->{op};
	my $t = type_of($e);
 
	if($op eq 'intlit') {
		my $tmp = new_tmp();
		emit("$tmp = const $t " . $e->{atoms}[0]);
		return $tmp;
	}
	if($op eq 'charlit') {
		my $tmp = new_tmp();
		emit("$tmp = const $t " . $e->{atoms}[0]);
		return $tmp;
	}
	if($op eq 'strlit') {
		my $tmp = new_tmp();
		my $content = $e->{atoms}[0];
		# two occurrences of the same literal are the same blob
		# (the .LCn is reused); distinct contents are distinct
		# blobs, no suffix merging.
		my $lbl = $STRMAP{$content};
		if(defined $lbl) {
			emit("$tmp = str $lbl");
			return $tmp;
		}
		$lbl = '.LC' . $STR_N++;
		$STRMAP{$content} = $lbl;
		my $s = $content;
		$s =~ s/^"//;
		$s =~ s/"$//;
		$s =~ s/([\\ ]|[^\x20-\x7E])/sprintf("\\x%02X", ord($1))/ge;
		emit("$tmp = str \"$s\"");
		return $tmp;
	}
	if($op eq 'ref') {
		my $id = symid_of($e);
		my $sym = $SYMS{$id};
		err_ctx("ref to unknown sym=$id") unless $sym;
		my $tmp = new_tmp();
		if($sym->{kind} eq 'gvar') {
			emit("$tmp = load $t sym=$id name=" . ($sym->{name} // '?'));
		}
		else {
			emit("$tmp = load $t $sym->{slot}");
		}
		return $tmp;
	}
	if($op eq 'binop') {
		my $a = flat_expr($e->{kids}[0]);
		my $b = flat_expr($e->{kids}[1]);
		my $tmp = new_tmp();
		my $bop = $e->{atoms}[0];
		if($bop =~ /^(==|!=|<|<=|>|>=)$/) {
			my $canon = {'==' => 'eq', '!=' => 'ne', '<' => 'lt', '<=' => 'le', '>' => 'gt', '>=' => 'ge'}->{$bop};
			# the comparison runs at the (already unified) OPERAND
			# width -- the node's own ': i32' is the RESULT type.
			my $lt = type_of($e->{kids}[0]);
			emit("$tmp = cmp $lt $canon $a, $b");
			return $tmp;
		}
		# signed/unsigned spellings: '/' '%' '>>' pick their form by
		# the operand's signedness (jtype3 already unified both sides).
		my %signed_ops = ('+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'sdiv', '%' => 'smod',
			'&' => 'and', '|' => 'or', '^' => 'xor', '<<' => 'shl', '>>' => 'sar');
		my %unsigned_ops = ('/' => 'udiv', '%' => 'umod', '>>' => 'shr');
		$bop = ($unsigned_ops{$bop} && $t =~ /^u/) ? $unsigned_ops{$bop} : $signed_ops{$bop} // $bop;
		emit("$tmp = $bop $t $a, $b");
		return $tmp;
	}
	if($op eq 'unop') {
		my $a = flat_expr($e->{kids}[0]);
		my $tmp = new_tmp();
		my $uop = $e->{atoms}[0] eq '-' ? 'neg' : 'not';
		emit("$tmp = $uop $t $a");
		return $tmp;
	}
	if($op eq 'cast') {
		my $src_t = type_of($e->{kids}[0]);
		my $a = flat_expr($e->{kids}[0]);
		my $tmp = new_tmp();
		emit("$tmp = cast $src_t $t $a");
		return $tmp;
	}
	if($op eq 'addr') {
		my $inner = $e->{kids}[0];
		if($inner->{op} eq 'ref' || $inner->{op} eq 'member' || $inner->{op} eq 'index') {
			return flat_addr($inner);
		}
		if($inner->{op} eq 'deref') {
			return flat_expr($inner->{kids}[0]);  # &*p == p
		}
		err_ctx("& of a non-lvalue");
	}
	if($op eq 'deref') {
		my $a = flat_expr($e->{kids}[0]);
		my $tmp = new_tmp();
		emit("$tmp = ld8 $a");
		return $tmp;
	}
	if($op eq 'member' || $op eq 'index') {
		# a scalar read through a struct/array lvalue: its address,
		# then a typed load at the field/element's width.
		my $addr = flat_addr($e);
		my $tmp = new_tmp();
		emit("$tmp = ld $t $addr");
		return $tmp;
	}
	if($op eq 'call') {
		my $id = $e->{atoms}[0];
		my $name = name_of($e);
		my $tmp = new_tmp();
		emit(join(' ', "$tmp = call $t sym=$id name=$name", call_args($e->{kids})));
		return $tmp;
	}
	if($op eq 'icall') {
		# an indirect call: the callee's ptr value is evaluated
		# first, then the args left to right, then the call.
		my ($callee, @args) = @{$e->{kids}};
		my $f = flat_expr($callee);
		my $tmp = new_tmp();
		emit(join(' ', "$tmp = icall $t $f", call_args(\@args)));
		return $tmp;
	}
	err_ctx("cannot flatten node '$op'");
}
 
# the argument groups of a call line: scalars are (T %tN), struct
# args become one (i64 %tN) per word, loaded out of the struct's
# address in order.
sub call_args {
	my ($kids) = @_;
	my @args;
	for my $a (@$kids) {
		my $at = type_of($a);
		if($at =~ /^struct\./) {
			my $addr = flat_addr($a);
			my @ws = emit_struct_words($addr, words_of($at));
			push @args, map { "(i64 $_)" } @ws;
		}
		else {
			push @args, '(' . $at . ' ' . flat_expr($a) . ')';
		}
	}
	return @args;
}
 
# the address of a struct/array lvalue as a %tN: a ref contributes its
# slot's/global's address, a member adds its word offset, an index adds
# idx*8 (elements are word-spaced), a deref is already an address.
sub flat_addr {
	my ($e) = @_;
	my $op = $e->{op};
	if($op eq 'ref') {
		my $id = symid_of($e);
		my $sym = $SYMS{$id};
		err_ctx("& of unknown sym=$id") unless $sym;
		my $tmp = new_tmp();
		if($sym->{kind} eq 'gvar' || $sym->{kind} eq 'func') {
			emit("$tmp = addrof sym=$id name=" . ($sym->{name} // '?'));
		}
		else {
			my $slot = $sym->{slot};
			# an array's base is its LOWEST slot: elements run UP
			# from it (byte-packed, increasing addresses), so a[0]
			# is the first byte of the array's whole slot range.
			$slot = '%v' . ($sym->{slotno} + $sym->{nwords} - 1) if type_of($e) =~ /^\[/;
			emit("$tmp = addrof $slot");
		}
		return $tmp;
	}
	if($op eq 'member') {
		my $base = flat_addr($e->{kids}[0]);
		my ($k) = map { /^k=(\d+)$/ ? $1 : () } @{$e->{atoms}};
		err_ctx('member without its field index') unless defined $k;
		# words run DOWNWARD: field k sits at base - 8*k.
		my $off = new_tmp();
		emit("$off = const i64 " . (-8 * $k));
		my $tmp = new_tmp();
		emit("$tmp = add i64 $base, $off");
		return $tmp;
	}
	if($op eq 'index') {
		my $base = flat_addr($e->{kids}[0]);
		my $idx = flat_expr($e->{kids}[1]);
		# byte-packed elements run UP from the array's base (which
		# is already the lowest slot): a[i] = base + i*stride.
		my $et = type_of($e->{kids}[0]) =~ /^\[(\d+)\](.+)$/ ? $2 : type_of($e->{kids}[0]);
		my $stride = byte_width($et);
		my $w = new_tmp();
		emit("$w = const i64 $stride");
		my $off = new_tmp();
		emit("$off = mul i64 $idx, $w");
		my $tmp = new_tmp();
		emit("$tmp = add i64 $base, $off");
		return $tmp;
	}
	if($op eq 'deref') {
		return flat_expr($e->{kids}[0]);
	}
	err_ctx("cannot take the address of node '$op'");
}
 
# a word-by-word copy of a struct out of its address into a local's
# slots (or: the words of a struct call ARG, produced as (i64 %tN)
# groups for the call line). returns nothing; emits nwords ld8/words.
sub emit_struct_words {
	my ($addr, $n) = @_;
	my @words;
	for my $k (0 .. $n - 1) {
		my $off = new_tmp();
		emit("$off = const i64 " . (-8 * $k));
		my $w = new_tmp();
		emit("$w = add i64 $addr, $off");
		my $v = new_tmp();
		emit("$v = ld8 $w");
		push @words, $v;
	}
	return @words;
}
 
 
sub lower_stmt {
	my ($s) = @_;
	my $op = $s->{op};
 
	if($op eq 'decl') {
		my $ty = $s->{atoms}[1];
		my $id = symid_of($s);
		# the slot line itself is pre-emitted by proc() (slots are
		# assigned in decl order, but all slot lines come first);
		# only the initializer store is lowered here.
		$SYMS{$id}{slot} = '%v' . $SYMS{$id}{slotno};
		my $init = find_kid($s, 'init');
		if($init) {
			my $e0 = $init->{kids}[0];
			if($e0->{op} eq 'initlist') {
				if($ty =~ /^\[/) {
					# byte-packed: element i at base + i*stride, where
					# base is the array's lowest slot.
					my $et = $ty =~ /^\[(\d+)\](.+)$/ ? $2 : 'i8';
					my $stride = byte_width($et);
					my $base = new_tmp();
					emit("$base = addrof %v" . ($SYMS{$id}{slotno} + $SYMS{$id}{nwords} - 1));
					for my $i (0 .. $#{$e0->{kids}}) {
						my $tmp = flat_expr($e0->{kids}[$i]);
						my $off = new_tmp();
						emit("$off = const i64 " . ($stride * $i));
						my $addr = new_tmp();
						emit("$addr = add i64 $base, $off");
						emit("st " . type_of($e0->{kids}[$i]) . " $tmp, $addr");
					}
				}
				else {
					# struct: one word per field, field i is slot i.
					for my $i (0 .. $#{$e0->{kids}}) {
						my $tmp = flat_expr($e0->{kids}[$i]);
						emit("st " . type_of($e0->{kids}[$i]) . " $tmp, %v" . ($SYMS{$id}{slotno} + $i));
					}
				}
			}
			elsif($ty =~ /^struct\./ && $e0->{op} eq 'call') {
				# struct init from a struct-returning call: the
				# result lands straight in this local's slots.
				my $name = name_of($e0);
				emit(join(' ', "scall " . words_of($ty) . " $name %v$SYMS{$id}{slotno}", call_args($e0->{kids})));
			}
			elsif($ty =~ /^struct\./) {
				# struct init from another struct lvalue: a word
				# copy out of its address into these slots.
				my $addr = flat_addr($e0);
				my @ws = emit_struct_words($addr, words_of($ty));
				for my $k (0 .. $#ws) {
					emit("st i64 $ws[$k], %v" . ($SYMS{$id}{slotno} + $k));
				}
			}
			else {
				my $tmp = flat_expr($e0);
				emit("st $ty $tmp, %v$SYMS{$id}{slotno}");
			}
		}
		return;
	}
	if($op eq 'assign') {
		my ($lhs, $rhs) = @{$s->{kids}};
		my $lt = type_of($lhs);
		if($lhs->{op} eq 'ref') {
			my $id = symid_of($lhs);
			my $sym = $SYMS{$id};
			err_ctx("assign to unknown sym=$id") unless $sym;
			if($lt =~ /^struct\./) {
				if($rhs->{op} eq 'call') {
					# a struct-returning call goes straight into
					# this local's slots (j3 guarantees the target
					# is a local, never a global).
					my $name = name_of($rhs);
					emit(join(' ', "scall " . words_of($lt) . " $name %v$sym->{slotno}", call_args($rhs->{kids})));
				}
				else {
					my $laddr = flat_addr($lhs);
					my $addr = flat_addr($rhs);
					my @ws = emit_struct_words($addr, words_of($lt));
					for my $k (0 .. $#ws) {
						my $off = new_tmp();
						emit("$off = const i64 " . (-8 * $k));
						my $w = new_tmp();
						emit("$w = add i64 $laddr, $off");
						emit("st8 $ws[$k], $w");
					}
				}
				return;
			}
			my $tmp = flat_expr($rhs);
			if($sym->{kind} eq 'gvar') {
				emit("st $lt $tmp, sym=$id name=" . ($sym->{name} // '?'));
			}
			else {
				emit("st $lt $tmp, $sym->{slot}");
			}
			return;
		}
		if($lhs->{op} eq 'member' || $lhs->{op} eq 'index') {
			my $addr = flat_addr($lhs);
			my $tmp = flat_expr($rhs);
			emit("st " . type_of($lhs) . " $tmp, $addr");
			return;
		}
		if($lhs->{op} eq 'deref') {
			my $addr = flat_expr($lhs->{kids}[0]);
			my $val = flat_expr($rhs);
			emit("st8 $val, $addr");
			return;
		}
		err_ctx("assign to a non-lvalue");
	}
	if($op eq 'call') {
		my $id = $s->{atoms}[0];
		my $name = name_of($s);
		my $t = type_of($s);
		emit(join(' ', "call $t sym=$id name=$name", call_args($s->{kids})));
		return;
	}
	if($op eq 'icall') {
		my ($callee, @args) = @{$s->{kids}};
		my $t = type_of($s);
		my $f = flat_expr($callee);
		emit(join(' ', "icall $t $f", call_args(\@args)));
		return;
	}
	if($op eq 'ret') {
		if(@{$s->{kids}}) {
			my $t = type_of($s->{kids}[0]);
			if($t =~ /^struct\./) {
				# a struct return is a word move out of a local's
				# slots (j3 guarantees the operand is a plain
				# variable of the same type).
				my $id = symid_of($s->{kids}[0]);
				emit("sret " . words_of($t) . " %v$SYMS{$id}{slotno}");
				return;
			}
			my $tmp = flat_expr($s->{kids}[0]);
			emit("ret $t $tmp");
		}
		else {
			emit('ret void');
		}
		return;
	}
	if($op eq 'if') {
		my $c = flat_expr($s->{kids}[0]);
		my $else = $s->{kids}[-1] && $s->{kids}[-1]{op} eq 'else' ? pop(@{$s->{kids}}) : undef;
		# I-prefixed labels: jdesugar4 already owns the L namespace
		# (its loop/switch labels pass through untouched), so j5's
		# own if-lowering labels can never collide with them.
		my $l_else = 'I' . $LABEL_N++;
		my $l_end = 'I' . $LABEL_N++;
		emit("if $c $l_else");
		lower_stmt($_) for @{$s->{kids}}[1 .. $#{$s->{kids}}];
		emit("goto $l_end") if $else;
		emit("label $l_else");
		if($else) {
			lower_stmt($_) for @{$else->{kids}};
			emit("label $l_end");
		}
		return;
	}
	if($op eq 'goto') {
		emit("goto $s->{atoms}[0]");
		return;
	}
	if($op eq 'label') {
		emit("label $s->{atoms}[0]");
		return;
	}
	stage_err("jlower5: unknown statement node: $op");
}
 
 
# locals get slots in decl order; two passes: count slots first so
# every %vN slot line (and the frame) is emitted before any body code,
# whatever order decls appear in. every aggregate takes one slot PER
# WORD (struct fields, array elements). returns the running count.
sub count_slots {
	my ($node, $count) = @_;
	my $walk;
	$walk = sub {
		my ($n) = @_;
		return unless ref $n;
		if($n->{op} eq 'decl') {
			my $id = symid_of($n);
			my $nw = words_of($n->{atoms}[1]);
			$SYMS{$id} = {type => $n->{atoms}[1], kind => 'local', name => $n->{atoms}[0], slotno => $count, nwords => $nw};
			$count += $nw;
		}
		$walk->($_) for @{$n->{kids}};
	};
	$walk->($node);
	return $count;
}
 
my $TARGET;     # the optional target header line, passed through verbatim
 
sub preproc {
	my ($text) = @_;
	# an optional first line "target MODE [key=value ...]" (nscc's -s
	# flag injects it between jdesugar4 and this stage) makes the lir
	# self-describing; it is passed through verbatim, j5 knows nothing
	# else about it.
	my @lines = split /\n/, $text;
	if(@lines && $lines[0] =~ /^target \S+( \S+=\S+)*$/) {
		$TARGET = shift @lines;
	}
	return read_tree(join("\n", @lines));
}
 
sub proc {
	my ($unit) = @_;
	my $out = defined $TARGET ? "$TARGET\n" : '';
	# string blobs are shared across the whole unit (.rodata is one
	# section), so the dedupe map lives at unit scope, not per func.
	%STRMAP = ();
	$STR_N = 0;
	# struct field counts come from the unit's own struct nodes.
	%STRUCTS = ();
	for my $top (@{$unit->{kids}}) {
		next unless $top->{op} eq 'struct';
		$STRUCTS{$top->{atoms}[0]} = [map { $_->{atoms}[1] } @{$top->{kids}}];
	}
 
	for my $top (@{$unit->{kids}}) {
		if($top->{op} eq 'func') {
			my ($name) = $top->{atoms}[0];
			my ($sym) = map { /^sym=(\d+)$/ ? $1 : () } @{$top->{atoms}};
			my ($linkage) = map { /^linkage=(.+)$/ ? uc($1) : () } @{$top->{atoms}};
			my ($ret) = map { /^ret=(.+)$/ ? $1 : () } @{$top->{atoms}};
			my $params = find_kid($top, 'params');
			my $body = find_kid($top, 'body');
 
			# params + body decls: sym table entries and slot numbers
			# (one slot per word).
			%SYMS = ();
			my $nslots = 0;
			my @param_slots;
			for my $p (@{$params->{kids}}) {
				my $id = symid_of($p);
				my $nw = words_of($p->{atoms}[1]);
				$SYMS{$id} = {type => $p->{atoms}[1], kind => 'local', name => $p->{atoms}[0], slotno => $nslots, nwords => $nw, slot => '%v' . $nslots};
				for my $k (0 .. $nw - 1) { push @param_slots, '%v' . ($nslots + $k); }
				$nslots += $nw;
			}
			$nslots = count_slots($body, $nslots);
			# globals referenced by this function live in the same
			# table, kind gvar -- built from the unit's gvar nodes.
			for my $g (@{$unit->{kids}}) {
				next unless $g->{op} eq 'gvar';
				$SYMS{symid_of($g)} = {type => $g->{atoms}[1], kind => 'gvar', name => $g->{atoms}[0], nwords => words_of($g->{atoms}[1])};
			}
			# functions too, kind func: &f takes a function's address
			# exactly the way &g takes a global's (addrof sym=N
			# name=X) -- a function is a symbol the same shape.
			for my $g (@{$unit->{kids}}) {
				next unless $g->{op} eq 'func' || $g->{op} eq 'fdecl';
				$SYMS{symid_of($g)} = {kind => 'func', name => $g->{atoms}[0]};
			}
 
			@OUT = ();
			$TMP_N = 0;
			$LABEL_N = 0;
			my $frame = 8 * $nslots;
			$out .= "func $name sym=$sym linkage=$linkage ret=$ret frame=$frame locals=$nslots\n";
			for my $id (sort { $SYMS{$a}{slotno} <=> $SYMS{$b}{slotno} } grep { ($SYMS{$_}{kind} // '') eq 'local' } keys %SYMS) {
				next unless defined $SYMS{$id}{slotno};
				my $nw = $SYMS{$id}{nwords} // 1;
				for my $k (0 .. $nw - 1) {
					my $off = -8 * ($SYMS{$id}{slotno} + $k + 1);
					my $ty = $k == 0 ? $SYMS{$id}{type} : 'word';
					$out .= $IND . "%v" . ($SYMS{$id}{slotno} + $k) . " = slot $ty $off(%rbp)\n";
				}
			}
			# params arrive in the sysv arg registers; each word of a
			# param spills into its slot, in order, one argp per word.
			for my $ps (@param_slots) {
				$out .= $IND . "param i64 $ps\n";
			}
			lower_stmt($_) for @{$body->{kids}};
			$out .= join("\n", @OUT) . (@OUT ? "\n" : '');
			$out .= "end\n";
			next;
		}
		if($top->{op} eq 'gvar') {
			my ($name) = $top->{atoms}[0];
			my ($sym) = map { /^sym=(\d+)$/ ? $1 : () } @{$top->{atoms}};
			my ($linkage) = map { /^linkage=(.+)$/ ? uc($1) : () } @{$top->{atoms}};
			my $ty = $top->{atoms}[1];
			my $init = find_kid($top, 'init');
			if($init) {
				my $e0 = $init->{kids}[0];
				if($e0->{op} eq 'initlist') {
					# one folded word per field/element.
					my @vals = map { $_->{kids}[0]{atoms}[0] } @{$e0->{kids}};
					$GVARS .= "gvar $name sym=$sym linkage=$linkage $ty " . join(' ', @vals) . "\n";
				}
				else {
					my $v = $e0->{kids}[0]{atoms}[0];
					$GVARS .= "gvar $name sym=$sym linkage=$linkage $ty $v\n";
				}
			}
			elsif($linkage eq 'GLOBAL') {
				# no initializer, but `global` -- a real defining
				# declaration, still needs real storage, just zeroed.
				# previously this whole branch just fell through to
				# `next` below with nothing appended to $GVARS at all,
				# silently dropping the variable from the rest of the
				# pipeline: it would still typecheck and get
				# referenced as name(%rip) wherever used, but jemit8
				# never saw it and never emitted a .bss/.data symbol
				# for it, leaving every reference an undefined
				# reference at link time. the "uninit:N" marker here
				# (N = exact byte size, computed now while $STRUCTS
				# and byte_width are still in scope -- jemit8 only
				# sees flattened text by this point and has no way to
				# re-derive a struct's size from its type name alone)
				# is what tells jemit8 to route this to .bss/.zero N
				# instead of the .data path.
				#
				# a plain (non-global) uninitialized declaration is
				# NOT a definition -- same as a bodyless function
				# prototype, it's a reference expecting real storage
				# from elsewhere (another file's `global`-initialized
				# or `global`-uninitialized declaration, or a
				# hand-written .s file), and correctly still falls
				# through to `next` below with no $GVARS line at all.
				# t23's cross-file counter reference depends on this
				# distinction: giving every uninitialized declaration
				# its own storage regardless of linkage (the first cut
				# of this fix) made a non-global reference in one file
				# collide with the real global definition in another.
				my $bytes;
				if($ty =~ /^\[(\d+)\](.+)$/) { $bytes = $1 * byte_width($2); }
				elsif($ty =~ /^struct\./) { $bytes = words_of($ty) * 8; }
				else { $bytes = byte_width($ty); }
				$GVARS .= "gvar $name sym=$sym linkage=$linkage $ty uninit:$bytes\n";
			}
			next;
		}
		# fdecl: nothing to lower.
	}
	$out .= $GVARS;
	return $out;
}
 
print proc(preproc($RAW));
powered by btf.