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

src/jscope2.pl


#!/usr/bin/env perl
# jscope2 -- the nscc symbol/scope stage, third stage of the nscc
# pipeline. owns: symbol tables, name resolution, declaration vs
# definition merging, the global keyword -> linkage flag, extern-style
# forward decls (a decl with no body/init never allocates anything and
# may stay unresolved forever -- linking is the user's problem, same as
# c), shadowing (a block-local may reuse an outer name; a second decl
# of the same name in the SAME scope is an error), and redefinition
# errors (two bodies for one function, two initializers for one global).
# break/continue outside any loop or switch is rejected here, while the
# loop/switch structure still exists to check against.
#
# output format .ast.j2: the same tree shape as the cst, but every
# (var NAME) is now (ref ID), every (call NAME ...) is (call ID name=X
# ...), every func/fdecl/gvar gains sym=N and linkage=local|global
# atoms, and every local decl gains sym=N. a (call NAME ...) whose NAME
# is no function but a variable becomes (icall (ref ID) ARG...), an
# indirect call through the variable's value; (addr (var NAME)) whose
# NAME is no variable but a function becomes (addr (ref ID)) with the
# function's sym (a function designator: it appears nowhere else). a
# function name wins in call position and a variable wins under &, so
# neither rule changes what any existing name meant. ids are assigned in
# walk order starting at 1 -- they are the unit's only identity for
# names from here on; the name= atom stays on calls (and gvar/func
# nodes) purely so jlower5/jselect6/jemit8 can spell the symbol in the
# final asm without a symbol table of their own.
#
# this stage's own mini pipeline: in -- preproc -- proc -- out, where
# preproc is the cst deserialization (jcplib::tree) and proc is the
# resolve proper.
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 write_tree mk find_kid);
 
my $RAW = do { local $/; <STDIN>; };
 
my %FUNCS;      # name -> sym
my %GVARS;      # name -> sym
my %TDEFS;      # typedef/enum name -> underlying type text (alias chain resolved here)
my %STRUCTS;    # struct name -> [field names/types]
my %ENUMS;      # enumerator name -> {id, enum => enum-name, value => expr node}
my $NEXT_ID = 1;
my $FILE = $ENV{NSCC_FILE} || 'input';
 
sub new_id { return $NEXT_ID++; }
 
sub err_at {
	my ($node, $msg) = @_;
	my ($at) = grep { /^at=/ } @{$node->{atoms}};
	$at //= 'at=?:?';
	$at =~ s/^at=//;
	stage_err("jscope2: $FILE:$at: $msg");
}
 
sub atoms_have {
	my ($node, $re) = @_;
	return scalar grep { /$re/ } @{$node->{atoms}};
}
 
# the builtin scalar type names (not struct/array/alias).
sub is_scalar_type {
	my ($t) = @_;
	return $t eq 'i8' || $t eq 'i16' || $t eq 'i32' || $t eq 'i64'
	    || $t eq 'u8' || $t eq 'u16' || $t eq 'u32' || $t eq 'u64'
	    || $t eq 'ptr' || $t eq 'void';
}
 
# resolve one type atom to a concrete type: [N]T recurses, struct.NAME
# validates, typedef/enum names follow their alias chain. returns undef
# when the atom is no type at all -- the caller reinterprets that case
# (an (IDENT) cast that was really a parenthesized expression).
sub resolve_type_atom {
	my ($t) = @_;
	return $t if is_scalar_type($t);
	if($t =~ /^\[(\d+)\](.+)$/) {
		my $inner = resolve_type_atom($2);
		return defined $inner ? "[$1]$inner" : undef;
	}
	if($t =~ /^struct\.(.+)$/) {
		stage_err('jscope2: unknown struct type struct.$1') unless $STRUCTS{$1};
		return $t;
	}
	my $depth = 0;
	while(defined $TDEFS{$t}) {
		$t = $TDEFS{$t};
		stage_err('jscope2: typedef cycle') if ++$depth > 100;
	}
	return $t if is_scalar_type($t) || $t =~ /^struct\./ || $t =~ /^\[/;
	return undef;
}
 
 
sub param_sig {
	my ($params) = @_;
	return join(',', map { $_->{atoms}[1] } @{$params->{kids}});
}
 
sub collect_toplevel {
	my ($unit) = @_;
	for my $top (@{$unit->{kids}}) {
		if($top->{op} eq 'func' || $top->{op} eq 'fdecl') {
			my $name = $top->{atoms}[0];
			my ($ret) = map { /^ret=(.+)$/ ? $1 : () } @{$top->{atoms}};
			my $params = find_kid($top, 'params');
			my $sig = param_sig($params);
			my $global = atoms_have($top, '^linkage=global$');
			my $has_body = $top->{op} eq 'func';
			if(my $sym = $FUNCS{$name}) {
				err_at($top, "redefinition of function '$name'") if $sym->{defined} && $has_body;
				err_at($top, "inconsistent signature for '$name': ($sig) vs ($sym->{sig})") unless $sig eq $sym->{sig};
				err_at($top, "inconsistent return type for '$name'") unless $ret eq $sym->{ret};
				# the DEFINITION's own linkage always wins, in either
				# direction: global i32 f(){} after a bare prototype
				# i32 f(); must upgrade a prototype-only 'local' up to
				# 'global' just as much as a non-global definition
				# downgrades one -- this is exactly what a shared
				# header (include "x.nsh") produces everywhere it's
				# included before the real global-linked definition
				# is reached, so getting only the downgrade direction
				# right (the bug this comment used to describe) made
				# a header's own defining file silently lose linkage
				# on everything it declared a prototype for first. a
				# bodyless (no-body) re-declaration, before or after
				# the real definition, still never touches linkage at
				# all -- only reached inside this $has_body block.
				$sym->{linkage} = $global ? 'global' : 'local' if $has_body;
				$sym->{defined} = 1 if $has_body;
				$top->{atoms} = [$name, "ret=$ret", "linkage=" . $sym->{linkage}, "sym=" . $sym->{id}, grep { /^at=/ } @{$top->{atoms}}];
			}
			else {
				my $sym = {id => new_id(), kind => 'func', name => $name, ret => $ret, sig => $sig,
					linkage => $global ? 'global' : 'local', defined => $has_body};
				$FUNCS{$name} = $sym;
				$top->{atoms} = [$name, "ret=$ret", "linkage=" . $sym->{linkage}, "sym=" . $sym->{id}, grep { /^at=/ } @{$top->{atoms}}];
			}
			next;
		}
		if($top->{op} eq 'gvar') {
			my $name = $top->{atoms}[0];
			my $ty = $top->{atoms}[1];
			my $global = atoms_have($top, '^linkage=global$');
			my $const = atoms_have($top, '^const=1$');
			my $init = find_kid($top, 'init');
			if(my $sym = $GVARS{$name}) {
				err_at($top, "redefinition of global '$name'") if $sym->{defined} && $init;
				err_at($top, "conflicting types for global '$name'") unless $ty eq $sym->{type};
				# same fix, same reasoning, as the function case above,
				# broadened one step further: a gvar occurrence can be
				# a real (initialized) definition OR a tentative one
				# (global, no initializer -- jlower5/jemit8 emit that
				# as a .comm symbol, real storage, just zeroed) -- both
				# carry a real opinion about linkage and both need to
				# be able to upgrade a prototype-only 'local' up to
				# 'global', not just the initialized case. a bare
				# reference (no global, no init) is pure reference,
				# never touches linkage in either direction.
				$sym->{linkage} = $global ? 'global' : 'local' if $init || $global;
				$sym->{defined} = 1 if $init;
				$top->{atoms} = [$name, $ty, "linkage=" . $sym->{linkage}, "sym=" . $sym->{id}, grep { /^at=|^const=/ } @{$top->{atoms}}];
			}
			else {
				my $sym = {id => new_id(), kind => 'gvar', name => $name, type => $ty,
					linkage => $global ? 'global' : 'local', defined => $init ? 1 : 0, const => $const};
				$GVARS{$name} = $sym;
				$top->{atoms} = [$name, $ty, "linkage=" . $sym->{linkage}, "sym=" . $sym->{id}, grep { /^at=|^const=/ } @{$top->{atoms}}];
			}
			next;
		}
		if($top->{op} eq 'typedef') {
			my $name = $top->{atoms}[0];
			err_at($top, "redefinition of type name '$name'") if exists $TDEFS{$name};
			# store the alias RAW; resolve_type_atom follows chains
			# when the name is actually used, so forward references
			# and re-aliasing both work.
			$TDEFS{$name} = $top->{atoms}[1];
			next;
		}
		if($top->{op} eq 'struct') {
			my $name = $top->{atoms}[0];
			err_at($top, "redefinition of struct '$name'") if exists $STRUCTS{$name};
			my %fields;
			my @order;
			for my $f (@{$top->{kids}}) {
				err_at($top, "duplicate field '$f->{atoms}[0]' in struct '$name'") if exists $fields{$f->{atoms}[0]};
				$fields{$f->{atoms}[0]} = 1;
				push @order, $f->{atoms}[1];
			}
			err_at($top, "struct '$name' has no fields") unless @order;
			$STRUCTS{$name} = \@order;
			next;
		}
		if($top->{op} eq 'enum') {
			my $name = $top->{atoms}[0];
			err_at($top, "redefinition of type name '$name'") if exists $TDEFS{$name};
			$TDEFS{$name} = 'i32';
			my $prev_id;
			for my $v (@{$top->{kids}}) {
				my $vn = $v->{atoms}[0];
				err_at($top, "redefinition of enumerator '$vn'") if exists $ENUMS{$vn};
				my $en = {id => new_id(), enum => $name, value => $v->{kids}[0]};
				$ENUMS{$vn} = $en;
				# an enumerator without '=' is the previous one + 1
				# (the first is 0); synthesized here so jtype3 folds
				# it exactly like an explicit value.
				$v->{kids} = [$v->{kids}[0]] if @{$v->{kids}};
				unless(@{$v->{kids}}) {
					$v->{kids} = [$prev_id ? mk('binop', ['+'], [mk('ref', [$prev_id]), mk('intlit', [1])]) : mk('intlit', [0])];
				}
				$v->{atoms} = [$vn, "sym=" . $en->{id}];
				$prev_id = $en->{id};
			}
			next;
		}
		stage_err("jscope2: unknown toplevel node: $top->{op}");
	}
	return;
}
 
# resolve every type atom in the tree (params, decls, gvars, fields,
# ret=, cast and sizeof(type) atoms), following typedef/enum aliases;
# drop the typedef decls (fully consumed here -- struct and enum nodes
# survive, jtype3 needs their fields/values); reinterpret an (IDENT)
# cast whose name is no type as the grouping parens it really was.
sub resolve_types_pass {
	my ($unit) = @_;
	my $walk;
	$walk = sub {
		my ($n) = @_;
		return $n unless ref $n;
		for my $i (0 .. $#{$n->{kids}}) {
			$n->{kids}[$i] = $walk->($n->{kids}[$i]);
		}
		if($n->{op} eq 'func' || $n->{op} eq 'fdecl') {
			$n->{atoms} = [map { /^ret=(.+)$/ ? 'ret=' . (resolve_type_atom($1) // (stage_err("jscope2: unknown type '$1'"), 'i32')) : $_ } @{$n->{atoms}}];
		}
		elsif($n->{op} eq 'param' || $n->{op} eq 'decl' || $n->{op} eq 'gvar' || $n->{op} eq 'field') {
			my $res = resolve_type_atom($n->{atoms}[1]);
			stage_err('jscope2: unknown type ' . $n->{atoms}[1]) unless defined $res;
			$n->{atoms}[1] = $res;
		}
		elsif($n->{op} eq 'cast') {
			my $res = resolve_type_atom($n->{atoms}[0]);
			if(defined $res) {
				$n->{atoms}[0] = $res;
			}
			else {
				# (x)e where x is no type: grouping parens.
				return $n->{kids}[0];
			}
		}
		elsif($n->{op} eq 'sizeof' && !@{$n->{kids}}) {
			my $res = resolve_type_atom($n->{atoms}[0]);
			if(defined $res) {
				$n->{atoms}[0] = $res;
			}
			else {
				# sizeof(x) where x is no type: the expression form.
				# the var node resolves through resolve_expr later.
				my $name = $n->{atoms}[0];
				$n->{atoms} = [];
				$n->{kids} = [mk('var', [$name], [])];
			}
		}
		return $n;
	};
	my @kept;
	for my $top (@{$unit->{kids}}) {
		my $n = $walk->($top);
		push @kept, $n unless $n->{op} eq 'typedef';
	}
	$unit->{kids} = \@kept;
	return;
}
 
 
# scopes: a stack of name->id hashes; the top scope is the innermost.
# params land in the function scope, every block opens a fresh scope.
 
my @SCOPES;
my @CTX;    # 'loop'|'switch' stack for break/continue checking
 
sub push_scope { push @SCOPES, {}; return; }
sub pop_scope { pop @SCOPES; return; }
 
sub declare_local {
	my ($node, $name, $ty) = @_;
	err_at($node, "redefinition of '$name' in the same scope") if exists $SCOPES[-1]{$name};
	my $id = new_id();
	$SCOPES[-1]{$name} = $id;
	$node->{atoms} = [$name, $ty, "sym=$id", grep { /^at=|^const=/ } @{$node->{atoms}}];
	return $id;
}
 
sub find_local {
	my ($name) = @_;
	for(my $i = $#SCOPES; $i >= 0; $i--) {
		return $SCOPES[$i]{$name} if exists $SCOPES[$i]{$name};
	}
	# a name that isn't local falls back to a global variable, then
	# to an enumerator -- functions are only reachable through
	# (call ...) and &name, never as a bare (var ...).
	if(my $sym = $GVARS{$name}) {
		return $sym->{id};
	}
	if(my $en = $ENUMS{$name}) {
		return $en->{id};
	}
	return undef;
}
 
sub lookup_local {
	my ($node, $name) = @_;
	my $id = find_local($name);
	return $id if defined $id;
	err_at($node, "'$name' is a function, not a value: take its address with &$name") if $FUNCS{$name};
	err_at($node, "use of undeclared identifier '$name'");
}
 
# lvalue expressions an assign/++/-- may target: a (soon to be
# resolved) var, a deref, a member, or an index.
sub check_lvalue {
	my ($node, $ctx) = @_;
	return if $node->{op} eq 'var' || $node->{op} eq 'deref' || $node->{op} eq 'member' || $node->{op} eq 'index';
	err_at($ctx // $node, "not an assignable expression");
}
 
sub resolve_expr {
	my ($e, $ctx) = @_;
	$ctx = $e unless $ctx;
	my $op = $e->{op};
 
	if($op eq 'var') {
		$e->{op} = 'ref';
		$e->{atoms} = [lookup_local($ctx, $e->{atoms}[0])];
		return;
	}
	if($op eq 'call') {
		my $name = $e->{atoms}[0];
		my $sym = $FUNCS{$name};
		if(!$sym) {
			# not a function: a variable of that name makes this an
			# indirect call through its value, (icall (ref ID) ARG...)
			# -- jtype3 insists it is a ptr. a function name always
			# wins in call position, so every call that compiled
			# before function pointers existed still means exactly
			# what it did.
			my $id = find_local($name);
			err_at($ctx, "call to undeclared function '$name'") unless defined $id;
			$e->{op} = 'icall';
			$e->{atoms} = [];
			$e->{kids} = [mk('ref', [$id], []), @{$e->{kids}}];
			resolve_expr($_, $ctx) for @{$e->{kids}}[1 .. $#{$e->{kids}}];
			return;
		}
		$e->{atoms} = [$sym->{id}, "name=$name"];
		resolve_expr($_, $ctx) for @{$e->{kids}};
		return;
	}
	if($op eq 'icall') {
		resolve_expr($_, $ctx) for @{$e->{kids}};
		return;
	}
	if($op eq 'addr' && $e->{kids}[0]{op} eq 'var') {
		# &name: a variable (local, global) wins, exactly as before;
		# only a name that is no variable but IS a function becomes
		# the function's address -- (addr (ref ID)) with the
		# function's sym, the same shape &globalvar has.
		my $name = $e->{kids}[0]{atoms}[0];
		if(!defined find_local($name) && (my $fs = $FUNCS{$name})) {
			$e->{kids}[0] = mk('ref', [$fs->{id}], []);
			return;
		}
		resolve_expr($e->{kids}[0], $ctx);
		return;
	}
	if($op eq 'assign') {
		check_lvalue($e->{kids}[0], $ctx);
		resolve_expr($e->{kids}[0], $ctx);
		resolve_expr($e->{kids}[1], $ctx);
		return;
	}
	if($op eq 'binop' || $op eq 'cond') {
		resolve_expr($_, $ctx) for @{$e->{kids}};
		return;
	}
	if($op eq 'unop' || $op eq 'addr' || $op eq 'deref' || $op eq 'cast' || $op eq 'sizeof') {
		resolve_expr($e->{kids}[0], $ctx) if @{$e->{kids}};
		return;
	}
	if($op eq 'member' || $op eq 'index' || $op eq 'initlist') {
		resolve_expr($_, $ctx) for @{$e->{kids}};
		return;
	}
	if($op eq 'preinc' || $op eq 'postinc' || $op eq 'predec' || $op eq 'postdec') {
		check_lvalue($e->{kids}[0], $ctx);
		resolve_expr($e->{kids}[0], $ctx);
		return;
	}
	# intlit/charlit/strlit/ref: leaves, nothing to resolve.
	return;
}
 
sub resolve_stmt {
	my ($s) = @_;
	my $op = $s->{op};
 
	if($op eq 'block') {
		push_scope();
		resolve_stmt($_) for @{$s->{kids}};
		pop_scope();
		return;
	}
	if($op eq 'decl') {
		declare_local($s, $s->{atoms}[0], $s->{atoms}[1]);
		my $init = find_kid($s, 'init');
		resolve_expr($init->{kids}[0], $s) if $init;
		return;
	}
	if($op eq 'stmt') {
		resolve_expr($s->{kids}[0], $s);
		return;
	}
	if($op eq 'assign') {
		resolve_expr($s);
		return;
	}
	if($op eq 'ret') {
		resolve_expr($s->{kids}[0], $s) if @{$s->{kids}};
		return;
	}
	if($op eq 'if') {
		resolve_expr($s->{kids}[0], $s);
		resolve_stmt($s->{kids}[1]);
		if(@{$s->{kids}} > 2) {
			my $else = $s->{kids}[2];
			resolve_stmt($else->{kids}[0]);
		}
		return;
	}
	if($op eq 'while') {
		resolve_expr($s->{kids}[0], $s);
		push @CTX, 'loop';
		resolve_stmt($s->{kids}[1]);
		pop @CTX;
		return;
	}
	if($op eq 'switch') {
		resolve_expr($s->{kids}[0], $s);
		push @CTX, 'switch';
		for my $k (@{$s->{kids}}[1 .. $#{$s->{kids}}]) {
			if($k->{op} eq 'case') { resolve_expr($k->{kids}[0], $k); resolve_stmt($_) for @{$k->{kids}}[1 .. $#{$k->{kids}}]; }
			else { resolve_stmt($_) for @{$k->{kids}}; }
		}
		pop @CTX;
		return;
	}
	if($op eq 'break' || $op eq 'continue') {
		err_at($s, "'$op' outside any loop or switch") unless @CTX;
		return;
	}
	if($op eq 'goto' || $op eq 'label') {
		return;  # never reach here in the cst, but harmless
	}
	stage_err("jscope2: unknown statement node: $op");
}
 
sub resolve_func_body {
	my ($func) = @_;
	my $body = find_kid($func, 'body');
	return unless $body;
	my $params = find_kid($func, 'params');
	push_scope();
	for my $p (@{$params->{kids}}) {
		declare_local($p, $p->{atoms}[0], $p->{atoms}[1]);
	}
	@CTX = ();
	resolve_stmt($_) for @{$body->{kids}};
	pop_scope();
	return;
}
 
sub preproc {
	my ($text) = @_;
	return read_tree($text);
}
 
sub proc {
	my ($unit) = @_;
	collect_toplevel($unit);
	resolve_types_pass($unit);
	# enum value exprs (they may reference earlier enumerators), and
	# gvar initializers (they may reference enumerators too).
	for my $top (@{$unit->{kids}}) {
		if($top->{op} eq 'enum') {
			for my $v (@{$top->{kids}}) {
				resolve_expr($v->{kids}[0], $v) if @{$v->{kids}};
			}
			next;
		}
		if($top->{op} eq 'gvar') {
			my $init = find_kid($top, 'init');
			resolve_expr($init->{kids}[0], $top) if $init;
			next;
		}
	}
	for my $top (@{$unit->{kids}}) {
		next unless $top->{op} eq 'func';
		resolve_func_body($top);
	}
	return write_tree($unit);
}
 
print proc(preproc($RAW));
powered by btf.