| git.druid.rocks | index | druid520 | nscc | src/ | jparse1.pl |
src/jparse1.pl
#!/usr/bin/env perl
# jparse1 -- the nsc parser, second stage of the nscc pipeline. owns:
# the nsc grammar, precedence climbing for expressions, and error
# recovery at statement boundaries (a bad statement is reported and
# skipped to the next ';' or '}' -- one error per line of noise, not a
# cascade of "expected ..." garbage after the first mistake). output
# format .cst.j1 is the indented s-expression tree from jcplib::tree:
# one node opener per line, leaf nodes inlined on their parent's
# opener line, closers glued to the end of the last line they close.
# every STATEMENT node carries an at=L:C atom (source position); pure
# expressions don't (j2..j4 report the enclosing statement's position
# for expression-level errors, and positions die at this stage anyway
# once every syntax error has been reported).
#
# the cst node vocabulary, complete:
# (unit toplevel...)
# (func NAME ret=T [linkage=global] (params (param NAME T [const=1])...) (body stmt...))
# (fdecl NAME ret=T [linkage=global] (params ...))
# (gvar NAME T [linkage=global] [const=1] (init EXPR|(initlist ...))?)
# (struct NAME (field NAME T)...) (typedef NAME T) (enum NAME (enumval NAME [E])...)
# (block stmt...) (decl NAME T [const=1] (init ...)?) (assign OP LVALUE EXPR)
# (stmt EXPR) (if C THEN (else ELSE)?) (while C BODY)
# (switch E (case VAL stmt...)* (default stmt...)?)
# (ret EXPR)? (break) (continue)
# (binop OP A B) (unop OP A) (addr A) (deref A)
# (member EXPR FIELD) (index BASE IDX) (initlist E...)
# (preinc L) (postinc L) (predec L) (postdec L) (call NAME ARG...)
# (icall CALLEE ARG...)
# (cast T E) (cond C T E) (sizeof T) (sizeof E)
# (intlit N) (charlit N) (strlit "...") (var NAME)
#
# a parameter list is explicit: () is an error, zero params is written
# (void). a type position accepts a builtin type keyword, struct NAME,
# or an IDENT (a typedef/enum name -- left raw here; jscope2 resolves
# aliases, and reinterprets an (IDENT) cast as grouping when the name
# is no type). i8 a[N] declares a fixed-size array; the type atom is
# [N]T. -> is sugar for (*p).f. name(args) is (call NAME ...) whether
# NAME turns out to be a function or a ptr variable (jscope2 decides);
# any other expr(args) is (icall EXPR ...), an indirect call through a
# ptr. const qualifies decls/params/gvars,
# never functions. unsigned exists only to be rejected with a pointer
# to u8/u16/u32/u64.
#
# this stage's own mini pipeline: in -- preproc -- proc -- out, where
# preproc is the token-line deserialization (jlex0's .toks.j0 format)
# and proc is the parse 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(write_tree mk);
my $RAW = do { local $/; <STDIN>; };
my %TYPES = map { $_ => 1 } qw(I8 I16 I32 I64 U8 U16 U32 U64 PTR VOID);
my %TYPE_TXT = (I8 => 'i8', I16 => 'i16', I32 => 'i32', I64 => 'i64',
U8 => 'u8', U16 => 'u16', U32 => 'u32', U64 => 'u64',
PTR => 'ptr', VOID => 'void');
my %ASSIGNOPS = map { $_ => 1 } qw(ASSIGN PLUSEQ MINUSEQ STAREQ SLASHEQ PERCENTEQ LSHIFTEQ RSHIFTEQ AMPEQ PIPEEQ CARETEQ);
my %ASSIGN_TXT = (ASSIGN => '=', PLUSEQ => '+=', MINUSEQ => '-=', STAREQ => '*=', SLASHEQ => '/=',
PERCENTEQ => '%=', LSHIFTEQ => '<<=', RSHIFTEQ => '>>=', AMPEQ => '&=', PIPEEQ => '|=', CARETEQ => '^=');
my %BINOP_PREC = ('OROR' => 1, 'ANDAND' => 2, 'PIPE' => 3, 'CARET' => 4, 'AMP' => 5,
'EQ' => 6, 'NE' => 6, 'LT' => 7, 'LE' => 7, 'GT' => 7, 'GE' => 7,
'LSHIFT' => 8, 'RSHIFT' => 8, 'PLUS' => 9, 'MINUS' => 9, 'STAR' => 10, 'SLASH' => 10, 'PERCENT' => 10);
my %BINOP_TXT = (OROR => '||', ANDAND => '&&', PIPE => '|', CARET => '^', AMP => '&',
EQ => '==', NE => '!=', LT => '<', LE => '<=', GT => '>', GE => '>=',
LSHIFT => '<<', RSHIFT => '>>', PLUS => '+', MINUS => '-', STAR => '*', SLASH => '/', PERCENT => '%');
my %UNOP_TXT = (MINUS => '-', TILDE => '~', BANG => '!');
my $TOKS; # parsed token arrayref
my $I; # cursor into it
my $FILE = $ENV{NSCC_FILE} || 'input';
sub tok_lncol {
my ($t) = @_;
return $t->{l} . ':' . $t->{c};
}
sub parse_err {
my ($t, $msg) = @_;
stage_err("jparse1: $FILE:" . tok_lncol($t) . ": $msg");
}
sub peek { return $TOKS->[$I]; }
sub peek2 { return $TOKS->[$I + 1]; }
sub next_tok {
my $t = $TOKS->[$I];
$I++ if $t && $t->{t} ne 'EOF';
return $t;
}
sub expect {
my ($type, $what) = @_;
my $t = peek();
parse_err($t, "expected $what, got " . ($t->{t} eq 'EOF' ? 'end of file' : "'$t->{v}'")) unless $t && $t->{t} eq $type;
return next_tok();
}
sub is_type_tok {
my ($t) = @_;
return $t && exists $TYPE_TXT{$t->{t}};
}
# a type may begin with a builtin type keyword, the struct keyword, or
# an IDENT (a typedef/enum name -- jscope2 resolves which; an IDENT
# that turns out not to name a type is handled there too).
sub tok_starts_type {
my ($t) = @_;
return $t && (is_type_tok($t) || $t->{t} eq 'STRUCT' || $t->{t} eq 'IDENT');
}
# parse a type, returning its text atom: a builtin name, a typedef/enum
# name left raw for jscope2, or struct.NAME for the struct keyword.
sub parse_type {
my $t = peek();
if(is_type_tok($t)) { return $TYPE_TXT{next_tok()->{t}}; }
if($t->{t} eq 'STRUCT') {
next_tok();
my $name = expect('IDENT', 'a struct name');
return "struct.$name->{v}";
}
if($t->{t} eq 'IDENT') { return next_tok()->{v}; }
parse_err($t, 'expected a type');
}
# { e, e, ... } -- a struct/array initializer list (only legal right
# after a decl's '='; jtype3 checks the count and the element types).
sub parse_initlist {
expect('LBRACE', "'{'");
my @els;
if(peek()->{t} ne 'RBRACE') {
push @els, parse_expr();
while(peek()->{t} eq 'COMMA') { next_tok(); push @els, parse_expr(); }
}
expect('RBRACE', "'}'");
return mk('initlist', [], \@els);
}
# decode a char/string literal's escapes (\n \t \r \0 \\ \' \" and
# \xNN); the surrounding quotes are kept on the text. hex escapes
# decode first, so the simple-escape pass below only ever sees a \x
# with malformed digits (an error).
sub decode_esc {
my ($s, $t) = @_;
my $body = substr($s, 1, -1);
$body =~ s{\\x([0-9A-Fa-f]{2})}{chr(hex($1))}ge;
$body =~ s{\\(.)}{
$1 eq 'n' ? "\n" : $1 eq 't' ? "\t" : $1 eq 'r' ? "\r" : $1 eq '0' ? "\0"
: $1 eq '\\' ? '\\' : $1 eq "'" ? "'" : $1 eq '"' ? '"'
: (parse_err($t, "unknown escape \\$1"));
}ge;
return substr($s, 0, 1) . $body . substr($s, -1);
}
sub at_of {
my ($t) = @_;
return 'at=' . tok_lncol($t);
}
sub parse_block {
my $open = expect('LBRACE', "'{'");
my @stmts;
while(1) {
my $t = peek();
parse_err($t, 'unexpected end of file in block') unless $t;
last if $t->{t} eq 'RBRACE';
last if $t->{t} eq 'EOF';
push @stmts, parse_stmt();
}
expect('RBRACE', "'}'");
return mk('block', [at_of($open)], \@stmts);
}
sub parse_stmt {
my $t = peek();
if($t->{t} eq 'LBRACE') { return parse_block(); }
if($t->{t} eq 'UNSIGNED') {
parse_err($t, "unsigned types are spelled u8/u16/u32/u64");
}
if($t->{t} eq 'INCLUDE') {
parse_err($t, 'malformed include directive -- expected exactly: include "path.nsh"; alone on its own line');
}
# a statement is a declaration when it starts with const, with a
# builtin type keyword or struct, or with an IDENT followed by
# another IDENT (typedef name + variable name) -- anything else
# IDENT-led (x = ..., x[i] = ..., x++;) is an expression.
my $is_decl = 0;
if($t->{t} eq 'CONST') { $is_decl = 1; }
elsif(is_type_tok($t) || $t->{t} eq 'STRUCT') { $is_decl = 1; }
elsif($t->{t} eq 'IDENT' && peek2() && peek2()->{t} eq 'IDENT') { $is_decl = 1; }
if($is_decl) {
my $const = '';
if($t->{t} eq 'CONST') { $const = 'const=1'; next_tok(); $t = peek(); }
my $ty = parse_type();
my $name = expect('IDENT', 'an identifier');
if(peek()->{t} eq 'LBRACKET') {
# i32 a[N] -- a fixed-size array; the type atom is [N]T.
next_tok();
my $n = expect('INTLIT', 'an array size');
parse_err($n, 'array size must be a positive decimal') unless $n->{v} =~ /^[1-9][0-9]*$/;
expect('RBRACKET', "']'");
$ty = "[$n->{v}]$ty";
}
if(peek()->{t} eq 'ASSIGN') {
next_tok();
my $init;
if(peek()->{t} eq 'LBRACE') {
$init = parse_initlist();
}
else {
$init = parse_expr();
}
expect('SEMI', "';'");
return mk('decl', [$name->{v}, $ty, $const, at_of($t)], [mk('init', [], [$init])]);
}
expect('SEMI', "';'");
return mk('decl', [$name->{v}, $ty, $const, at_of($t)], []);
}
if($t->{t} eq 'IF') {
next_tok();
expect('LPAREN', "'('");
my $c = parse_expr();
expect('RPAREN', "')'");
my $then = parse_stmt();
my @kids = ($c, $then);
if(peek()->{t} eq 'ELSE') {
next_tok();
push @kids, mk('else', [], [parse_stmt()]);
}
return mk('if', [at_of($t)], \@kids);
}
if($t->{t} eq 'WHILE') {
next_tok();
expect('LPAREN', "'('");
my $c = parse_expr();
expect('RPAREN', "')'");
my $body = parse_stmt();
return mk('while', [at_of($t)], [$c, $body]);
}
if($t->{t} eq 'SWITCH') {
next_tok();
expect('LPAREN', "'('");
my $e = parse_expr();
expect('RPAREN', "')'");
expect('LBRACE', "'{'");
my @kids = ($e);
my $seen_default = 0;
while(1) {
my $c = peek();
parse_err($c, 'unexpected end of file in switch') unless $c;
last if $c->{t} eq 'RBRACE' || $c->{t} eq 'EOF';
if($c->{t} eq 'CASE') {
next_tok();
my $val = parse_unary();
expect('COLON', "':'");
my @stmts;
while(1) {
my $s = peek();
last if $s->{t} eq 'CASE' || $s->{t} eq 'DEFAULT' || $s->{t} eq 'RBRACE' || $s->{t} eq 'EOF';
push @stmts, parse_stmt();
}
push @kids, mk('case', [], [$val, @stmts]);
next;
}
if($c->{t} eq 'DEFAULT') {
parse_err($c, 'duplicate default in switch') if $seen_default;
$seen_default = 1;
next_tok();
expect('COLON', "':'");
my @stmts;
while(1) {
my $s = peek();
last if $s->{t} eq 'CASE' || $s->{t} eq 'DEFAULT' || $s->{t} eq 'RBRACE' || $s->{t} eq 'EOF';
push @stmts, parse_stmt();
}
push @kids, mk('default', [], \@stmts);
next;
}
parse_err($c, "expected 'case', 'default' or '}', got '$c->{v}'");
}
expect('RBRACE', "'}'");
return mk('switch', [at_of($t)], \@kids);
}
if($t->{t} eq 'RETURN') {
next_tok();
my $at = at_of($t);
if(peek()->{t} eq 'SEMI') { next_tok(); return mk('ret', [$at], []); }
my $e = parse_expr();
expect('SEMI', "';'");
return mk('ret', [$at], [$e]);
}
if($t->{t} eq 'BREAK' || $t->{t} eq 'CONTINUE') {
my $op = lc($t->{t});
next_tok();
expect('SEMI', "';'");
return mk($op, [at_of($t)], []);
}
if($t->{t} eq 'SEMI') {
# a bare ';' is a no-op statement: skipped entirely, no node.
next_tok();
return mk('block', [at_of($t)], []);
}
# anything else: an expression statement (assignment, call, ++ ...).
my $e = parse_expr();
expect('SEMI', "';'");
return mk('stmt', [at_of($t)], [$e]);
}
sub parse_expr { return parse_assign(); }
sub parse_assign {
my $lhs = parse_ternary();
if(exists $ASSIGNOPS{peek()->{t}}) {
my $op = next_tok();
my $rhs = parse_assign();
return mk('assign', [$ASSIGN_TXT{$op->{t}}], [$lhs, $rhs]);
}
return $lhs;
}
sub parse_ternary {
my $c = parse_binary(1);
if(peek()->{t} eq 'QUES') {
next_tok();
my $t = parse_expr();
expect('COLON', "':'");
my $e = parse_assign();
return mk('cond', [], [$c, $t, $e]);
}
return $c;
}
sub parse_binary {
my ($minprec) = @_;
my $lhs = parse_unary();
while(exists $BINOP_PREC{peek()->{t}} && $BINOP_PREC{peek()->{t}} >= $minprec) {
my $op = next_tok();
my $rhs = parse_binary($BINOP_PREC{$op->{t}} + 1);
$lhs = mk('binop', [$BINOP_TXT{$op->{t}}], [$lhs, $rhs]);
}
return $lhs;
}
sub parse_unary {
my $t = peek();
if(exists $UNOP_TXT{$t->{t}}) {
next_tok();
my $e = parse_unary();
return mk('unop', [$UNOP_TXT{$t->{t}}], [$e]);
}
if($t->{t} eq 'AMP') { next_tok(); return mk('addr', [], [parse_unary()]); }
if($t->{t} eq 'STAR') { next_tok(); return mk('deref', [], [parse_unary()]); }
if($t->{t} eq 'INC') { next_tok(); return mk('preinc', [], [parse_unary()]); }
if($t->{t} eq 'DEC') { next_tok(); return mk('predec', [], [parse_unary()]); }
return parse_postfix();
}
sub parse_postfix {
my $e = parse_primary();
while(1) {
my $t = peek();
if($t->{t} eq 'LPAREN') {
next_tok();
my @args;
if(peek()->{t} ne 'RPAREN') {
push @args, parse_assign();
while(peek()->{t} eq 'COMMA') { next_tok(); push @args, parse_assign(); }
}
expect('RPAREN', "')'");
# a bare-name callee is (call NAME ARG...): jscope2 decides
# whether NAME is a function (a static call, the common
# case) or a ptr variable (an indirect call). any other
# callee expression is (icall CALLEE ARG...) -- always an
# indirect call through a ptr value, checked by jtype3.
# there is no (*fp)(args) form: *fp is an 8-byte load.
if($e->{op} eq 'var') {
$e = mk('call', [$e->{atoms}[0]], \@args);
}
else {
$e = mk('icall', [], [$e, @args]);
}
next;
}
if($t->{t} eq 'DOT') {
next_tok();
my $n = expect('IDENT', 'a field name');
$e = mk('member', [$n->{v}], [$e]);
next;
}
if($t->{t} eq 'ARROW') {
next_tok();
my $n = expect('IDENT', 'a field name');
$e = mk('member', [$n->{v}], [mk('deref', [], [$e])]);
next;
}
if($t->{t} eq 'LBRACKET') {
next_tok();
my $i = parse_expr();
expect('RBRACKET', "']'");
$e = mk('index', [], [$e, $i]);
next;
}
if($t->{t} eq 'INC') { next_tok(); $e = mk('postinc', [], [$e]); next; }
if($t->{t} eq 'DEC') { next_tok(); $e = mk('postdec', [], [$e]); next; }
last;
}
return $e;
}
sub parse_primary {
my $t = peek();
parse_err($t, 'expected an expression') unless $t;
if($t->{t} eq 'INTLIT') {
next_tok();
return mk('intlit', [$t->{v}], []);
}
if($t->{t} eq 'CHARLIT') {
next_tok();
my $s = decode_esc($t->{v}, $t);
return mk('charlit', [ord(substr($s, 1, 1))], []);
}
if($t->{t} eq 'STRLIT') {
next_tok();
return mk('strlit', [decode_esc($t->{v}, $t)], []);
}
if($t->{t} eq 'IDENT') {
next_tok();
return mk('var', [$t->{v}], []);
}
if($t->{t} eq 'SIZEOF') {
next_tok();
expect('LPAREN', "'('");
# sizeof(type) is an atom-carrying node; sizeof(expr) is a
# kid-carrying node. jtype3 folds both (compile-time
# constant) and rejects sizeof(void).
if(tok_starts_type(peek())) {
my $ty = parse_type();
expect('RPAREN', "')'");
return mk('sizeof', [$ty], []);
}
my $e = parse_expr();
expect('RPAREN', "')'");
return mk('sizeof', [], [$e]);
}
if($t->{t} eq 'UNSIGNED') {
parse_err($t, "unsigned types are spelled u8/u16/u32/u64");
}
if($t->{t} eq 'INCLUDE') {
parse_err($t, 'malformed include directive -- expected exactly: include "path.nsh"; alone on its own line');
}
if($t->{t} eq 'LPAREN') {
# (type)expr is a cast; (expr) is grouping. when the next
# token after '(' could start a type (builtin, struct, or an
# IDENT -- a typedef name), it parses as a cast and jscope2
# reinterprets the IDENT case as grouping when the name turns
# out not to be a type.
if(tok_starts_type(peek2()) && $TOKS->[$I + 1] && $TOKS->[$I + 2] && $TOKS->[$I + 2]->{t} eq 'RPAREN') {
next_tok();
my $ty = parse_type();
next_tok(); # RPAREN
return mk('cast', [$ty], [parse_unary()]);
}
next_tok();
my $e = parse_expr();
expect('RPAREN', "')'");
return $e;
}
parse_err($t, "unexpected token '$t->{v}'");
}
sub parse_params {
my @params;
my $lparen = expect('LPAREN', "'('");
# nsc is explicit about zero params: () is an error, (void) is
# the one way to say "no parameters".
parse_err($lparen, 'empty parameter list: write (void)') if peek()->{t} eq 'RPAREN';
if(peek()->{t} eq 'VOID' && $TOKS->[$I + 1]->{t} eq 'RPAREN') {
next_tok();
expect('RPAREN', "')'");
return mk('params', [], []);
}
while(1) {
my $t = peek();
parse_err($t, 'expected a parameter type') unless tok_starts_type($t) || $t->{t} eq 'CONST';
parse_err($t, 'void must stand alone: write (void)') if $t->{t} eq 'VOID';
my $const = '';
if($t->{t} eq 'CONST') { $const = 'const=1'; next_tok(); }
my $ty = parse_type();
# extern-style decls may leave a parameter unnamed
# (i32 fwd(i32);) -- '_' is the placeholder; a function
# DEFINITION rejects it (parse_toplevel checks once it
# knows a body follows).
my $name = '_';
my $nametok;
if(peek()->{t} eq 'IDENT') { $nametok = next_tok(); $name = $nametok->{v}; }
# nsc has no array values, so an array parameter is an error
# (pass &a[0] instead) -- spelled out, not a bare parse stop.
parse_err($nametok // peek(), 'array parameters are not allowed: pass &a[0]') if peek()->{t} eq 'LBRACKET';
push @params, mk('param', [$name, $ty, $const], []);
last unless peek()->{t} eq 'COMMA';
next_tok();
}
expect('RPAREN', "')'");
return mk('params', [], \@params);
}
sub parse_toplevel {
my $t = peek();
# struct declarations: STRUCT IDENT { ... }; -- a struct-TYPED
# decl (func or var) is STRUCT IDENT IDENT/( instead, and falls
# through to the generic path below (parse_type knows struct).
if($t->{t} eq 'STRUCT' && $TOKS->[$I + 1] && $TOKS->[$I + 1]->{t} eq 'IDENT' && $TOKS->[$I + 2] && $TOKS->[$I + 2]->{t} eq 'LBRACE') {
next_tok();
my $name = expect('IDENT', 'a struct name');
expect('LBRACE', "'{'");
my @fields;
while(peek()->{t} ne 'RBRACE') {
my $ft = peek();
parse_err($ft, 'const struct fields are not supported') if $ft->{t} eq 'CONST';
parse_err($ft, 'expected a field type') unless tok_starts_type($ft);
my $fty = parse_type();
my $fname = expect('IDENT', 'a field name');
expect('SEMI', "';'");
push @fields, mk('field', [$fname->{v}, $fty], []);
}
expect('RBRACE', "'}'");
expect('SEMI', "';'");
return mk('struct', [$name->{v}, at_of($t)], \@fields);
}
if($t->{t} eq 'TYPEDEF') {
next_tok();
my $ty = parse_type();
my $name = expect('IDENT', 'a typedef name');
expect('SEMI', "';'");
return mk('typedef', [$name->{v}, $ty, at_of($t)], []);
}
if($t->{t} eq 'ENUM') {
next_tok();
my $name = expect('IDENT', 'an enum name');
expect('LBRACE', "'{'");
my @vals;
while(peek()->{t} ne 'RBRACE') {
my $vn = expect('IDENT', 'an enumerator name');
my $v;
if(peek()->{t} eq 'ASSIGN') { next_tok(); $v = parse_expr(); }
push @vals, mk('enumval', [$vn->{v}], [$v ? ($v) : ()]);
last unless peek()->{t} eq 'COMMA';
next_tok();
}
expect('RBRACE', "'}'");
expect('SEMI', "';'");
return mk('enum', [$name->{v}, at_of($t)], \@vals);
}
if($t->{t} eq 'UNSIGNED') {
parse_err($t, "unsigned types are spelled u8/u16/u32/u64");
}
if($t->{t} eq 'INCLUDE') {
parse_err($t, 'malformed include directive -- expected exactly: include "path.nsh"; alone on its own line');
}
my $linkage = '';
if($t->{t} eq 'GLOBAL') { $linkage = 'linkage=global'; next_tok(); $t = peek(); }
my $const = '';
if($t->{t} eq 'CONST') { $const = 'const=1'; next_tok(); $t = peek(); }
parse_err($t, 'expected a type at toplevel') unless tok_starts_type($t);
my $ty = parse_type();
my $name = expect('IDENT', 'a name');
my $at = at_of($t);
if(peek()->{t} eq 'LBRACKET') {
# i32 a[N] -- a fixed-size global array; the type atom is
# [N]T.
next_tok();
my $n = expect('INTLIT', 'an array size');
parse_err($n, 'array size must be a positive decimal') unless $n->{v} =~ /^[1-9][0-9]*$/;
expect('RBRACKET', "']'");
$ty = "[$n->{v}]$ty";
}
if(peek()->{t} eq 'LPAREN') {
parse_err($name, 'const cannot qualify a function') if $const;
my $params = parse_params();
if(peek()->{t} eq 'LBRACE') {
for my $p (@{$params->{kids}}) {
parse_err($name, "parameter '_' needs a name in a function definition") if $p->{atoms}[0] eq '_';
}
my $body = mk('body', [], [parse_block()]);
return mk('func', [$name->{v}, "ret=$ty", grep { length } ($linkage), $at], [$params, $body]);
}
expect('SEMI', "';'");
return mk('fdecl', [$name->{v}, "ret=$ty", grep { length } ($linkage), $at], [$params]);
}
my $init;
if(peek()->{t} eq 'ASSIGN') {
next_tok();
if(peek()->{t} eq 'LBRACE') {
$init = parse_initlist();
}
else {
$init = parse_expr();
}
}
expect('SEMI', "';'");
return mk('gvar', [$name->{v}, $ty, grep { length } ($linkage, $const), $at], [$init ? mk('init', [], [$init]) : ()]);
}
# deserialize jlex0's .toks.j0: "TOK TEXT L:C" per line; STRLIT and
# CHARLIT lexemes arrive \xHH-escaped (they may contain spaces) and are
# unescaped back to their source text before parse.
sub preproc {
my ($text) = @_;
my @toks;
for my $line (split /\n/, $text) {
next unless $line =~ /\S/;
my ($t, $v, $pos) = split / /, $line, 3;
stage_err("jparse1: bad token line: $line") unless defined $pos;
my ($l, $c) = split /:/, $pos, 2;
$v =~ s/\\x([0-9A-Fa-f]{2})/chr(hex($1))/ge if $t eq 'STRLIT' || $t eq 'CHARLIT';
push @toks, {t => $t, v => $v, l => $l, c => $c};
}
stage_err('jparse1: empty token stream') unless @toks;
return \@toks;
}
sub proc {
my ($toks) = @_;
$TOKS = $toks;
$I = 0;
my @top;
while(peek()->{t} ne 'EOF') { push @top, parse_toplevel(); }
my $unit = mk('unit', [], \@top);
return write_tree($unit);
}
print proc(preproc($RAW));