shithub: mc

ref: 65c3b339b547b49d1c1d11c56d86ca81f756e9be
dir: /mbld/parse.myr/

View raw version
use std

use "types"
use "util"
use "opts"
use "syssel"

pkg bld =
	const load	: (b : build# -> bool)
	$noret const failparse : (p : parser#, msg : byte[:], args : ... -> void)
;;

type parser = struct
	/* parse input */
	data	: byte[:]
	rest	: byte[:]
	fname	: byte[:]
	fdir	: byte[:]	/* directory relative to base */
	basedir	: byte[:]
	line	: int

	/* all targets */
	targs	: targ[:]

	/* default parameter values */
	incpath	: byte[:][:]
	libdeps	: byte[:][:]
	tstdeps	: byte[:][:]
	runtime	: byte[:]
	ldscript: byte[:]
	install	: bool

	/* extracted data for further parsing */
	subdirs	: byte[:][:]
	targsel	: syssel((byte[:], targ))#
;;

const load = {b
	var ok, sel
	var targs

	sel = mksyssel(b, "mbld", 0, "mbld")
	ok = loadall(b, "bld.proj", "", sel)

	targs = sysselfin(sel)
	for (name, targ) : targs
		std.slpush(&b.all, name)
		std.htput(b.targs, name, targ)
	;;

	if ok
		ok = sortdeps(b)
	;;
	std.slfree(targs)
	-> ok
}

const sortdeps = {b
	var looped
	var marked
	var all

	all = [][:]
	looped = std.mkht()
	marked = std.mkht()
	for dep : b.all
		match gettarg(b.targs, dep)
		| `Bin _:	all = visit(all, b, "all", dep, looped, marked)
		| `Lib _:	all = visit(all, b, "all", dep, looped, marked)
		| targ:		std.slpush(&all, dep)
		;;
	;;
	std.htfree(marked)
	std.htfree(looped)
	std.slfree(b.all)
	b.all = all
	-> true
}

const visit = {all, b, parent, targ, looped, marked
	if std.hthas(looped, targ)
		std.fatal("cycle in build depgraph involving {}\n", targ)
	elif std.hthas(marked, targ)
		-> all
	;;

	std.htput(looped, targ, true)
	for (dir, lib, dep) : getdeps(b, parent, targ)
		all = visit(all, b, targ, dep, looped, marked)
	;;
	std.htdel(looped, targ)
	std.htput(marked, targ, true)
	-> std.slpush(&all, targ)
}

const getdeps = {b, parent, targname
	match std.htget(b.targs, targname)
	| `std.Some targ:
		match targ
		| `Bin t:	-> t.libdeps
		| `Lib t:	-> t.libdeps
		| _:	std.fatal("{} depends on non-library target", parent)
		;;
	| `std.None:
		std.fatal("{}: could not find dependency {}\n", parent, targname)
	;;
}

/* recursively load all bld.{proj,sub} files in project */
const loadall = {b, path, dir, sel
	var p : parser#
	var subbld, subproj, ok
	var curbase

	p = mkparser(path, dir, b.basedir, sel)
	ok = bld.parse(b, p, "")
	for targ : p.targs
		match targ
		| `Bin t:	setmyropt(p, t)
		| `Lib t:	setmyropt(p, t)
		| _:	/* only myr targets have settable defaults so far */
		;;
	;;

	for sub : p.subdirs
		subbld = std.pathcat(sub, "bld.sub")
		subproj = std.pathcat(sub, "bld.proj")
		/*
		bld.sub is a subbuild. It doesn't change the
		build root.
		*/
		if std.fexists(subbld)
			loadall(b, subbld, sub, sel)
		/*
		bld.proj reroots the project -- @/
		is relative to the most recent bld.proj
		in the heirarchy.
		*/
		elif std.fexists(sub)
			curbase = b.basedir
			b.basedir = sub
			loadall(b, subproj, sub, sel)
			b.basedir = curbase
		else
			std.fatal("could not open {} or {} \n", subbld, subproj)
		;;
		std.slfree(subbld)
		std.slfree(subproj)
	;;
	freeparser(p)
	-> ok
}

const setmyropt = {p, t
	var libdep, tstdep

	t.install = t.install && p.install
	if t.runtime.len == 0
		t.runtime = p.runtime
	;;
	if t.ldscript.len == 0
		t.ldscript = p.ldscript
	;;

	std.sljoin(&t.incpath, p.incpath)
	for l : p.libdeps
		libdep = libpath(p, l)
		std.slpush(&t.libdeps, libdep)
	;;
	for l : p.tstdeps
		tstdep = libpath(p, l)
		std.slpush(&t.tstdeps, tstdep)
	;;
}

const parse = {b, p, path
	while true
		if !target(b, p)
			break
		;;
	;;
	skipspace(p)
	if p.rest.len > 0
		failparse(p, "junk in file near {}\n", p.rest[:std.min(p.rest.len, 10)])
		-> false
	;;
	-> true
}

const target = {b, p
	match word(p)
	/* targets */
	| `std.Some "bin":	bintarget(b, p)
	| `std.Some "lib":	libtarget(b, p)
	| `std.Some "test":	testtarget(b, p)
	| `std.Some "bench":	benchtarget(b, p)
	| `std.Some "gen":	cmdtarget(b, p, "gen", false)
	| `std.Some "cmd":	cmdtarget(b, p, "cmd", true)
	| `std.Some "data":	datatarget(b, p, "data")
	| `std.Some "man":	mantarget(b, p)
	| `std.Some "sub":	subtarget(b, p)
	/* global attributes */
	| `std.Some "incpath":	incpath(b, p, &p.incpath, "incpath")
	| `std.Some "libdeps":	incpath(b, p, &p.libdeps, "libdeps")
	| `std.Some "testdeps":	incpath(b, p, &p.tstdeps, "testdeps")
	| `std.Some "runtime":	p.runtime = expectword(b, p, "runtime")
	| `std.Some "ldscript":	p.runtime = expectword(b, p, "ldscript")
	| `std.Some "noinst":	p.install = false
	/* no word */
	| `std.Some targtype:	failparse(p, "unknown keyword {}\n", targtype)
	| `std.None:	-> false
	;;
	-> true
}

const mkparser = {path, dir, basedir, sel
	var p

	p = std.mk([
		.line = 1,
		.fname = std.sldup(path),
		.fdir = std.sldup(dir),
		.basedir = std.sldup(basedir),
		.targsel = sel,
		.incpath = [][:],
		.libdeps = [][:],
		.tstdeps = [][:],
		.runtime = "",
		.ldscript = "",
		.install = true,
	])
	match std.slurp(path)
	| `std.Ok d:	p.data = d
	| `std.Err _:	std.fatal("could not open '{}'\n", path)
	;;
	p.rest = p.data
	-> p
}

const freeparser = {p
	std.slfree(p.fname)
	std.slfree(p.fdir)
	std.slfree(p.basedir)
	std.slfree(p.subdirs)
	std.slfree(p.data)
	std.free(p)
}

/* <name>: '=' wordlist ';;' */
const incpath = {b, p, words, name
	skipspace(p)
	if !matchc(p, '=')
		failparse(p, "expected '=' after {}\n", name)
	;;
	match wordlist(p)
	| `std.Some path:	words# = path
	| `std.None:
	;;
	skipspace(p)
	if !matchc(p, ';') || !matchc(p, ';')
		failparse(p, "expected ';;' after {} list\n", name)
	;;
}


const expectword = {b, p, attr
	match word(p)
	| `std.Some w:	-> w
	| `std.None:	failparse(p, "expected word after {}\n", attr)
	;;
}

/* bintarget: myrtarget */
const bintarget = {b, p
	var t
	t = myrtarget(b, p, "bin")
	addtarg(p, b, t.name, t.tags, `Bin t)
}

/* libtarget: myrtarget */
const libtarget = {b, p
	var t
	t = myrtarget(b, p, "lib")
	t.islib = true
	addtarg(p, b, t.name, t.tags, `Lib t)
}

/* testtarget: myrtarget */
const testtarget = {b, p
	var t
	t = myrtarget(b, p, "test")
	t.istest = true
	t.install = false
	addtarg(p, b, t.name, t.tags, `Bin t)
}

/* benchtarget: myrtarget */
const benchtarget = {b, p
	var t
	t = myrtarget(b, p, "bench")
	t.isbench = true
	t.install = false
	addtarg(p, b, t.name, t.tags, `Bin t)
}

/* mantarget: anontarget */
const mantarget = {b, p
	var targ

	targ = std.mk([
		.dir=std.sldup(p.fdir),
		.pages=anontarget(p, "man")
	])
	addtarg(p, b, "__man__", [][:], `Man targ)
}

/* subtarget : anontarget */
const subtarget = {b, p
	var subs

	subs = anontarget(p, "sub")
	for s : subs
		std.slpush(&p.subdirs, std.pathcat(p.fdir, s))
	;;
}

/* gentarget: wordlist {attrs} = wordlist ;; */
const cmdtarget = {b, p, cmd, iscmd
	var outlist, deplist, cmdlist, tags
	var durable, istest, isbench
	var attrs
	var gt

	match wordlist(p)
	| `std.None:	failparse(p, "{} target missing output files\n", cmd)
	| `std.Some out:
		if iscmd && out.len != 1
			failparse(p, "{} target takes one name\n", cmd)
		;;
		outlist = out
	;;

	match attrlist(p)
	| `std.Some al:	attrs = al
	| `std.None:	failparse(p, "invalid attr list for {}\n", outlist[0])
	;;

	skipspace(p)
	if !matchc(p, '=')
		failparse(p, "expected '=' after '{} {}'\n", cmd, outlist[outlist.len-1])
	;;

	match wordlist(p)
	| `std.None:	failparse(p, "{} target missing command\n", cmd)
	| `std.Some cl:
		cmdlist = cl
	;;

	if !matchc(p, ';') || !matchc(p, ';')
		failparse(p, "expected ';;' terminating genfile command, got {}\n", peekc(p))
	;;

	durable = false
	istest = false
	isbench = false
	deplist = [][:]
	tags = [][:]
	for elt : attrs
		match elt
		| ("durable", ""):	durable = true
		| ("test", ""):		istest = true
		| ("bench", ""):	isbench = true
		| ("notest", ""):	istest = false
		| ("dep", depname):	std.slpush(&deplist, depname)
		| ("tag", tag):	std.slpush(&tags, tag)
		| (attr, ""):
			failparse(p, "attribute {} not valid on {} targets\n", attr, cmd)
		| (attr, val):
			failparse(p, "attribute {}={} not valid on {} targets\n", attr, val, cmd)
		;;
	;;

	gt = std.mk([
		.dir=std.sldup(p.fdir),
		.gen=outlist,
		.durable=durable,
		.istest=istest,
		.isbench=isbench,
		.deps=deplist,
		.cmd=cmdlist,
		.tags=tags,
	])
	for o : outlist
		if iscmd
			addtarg(p, b, o, gt.tags, `Cmd gt)
		else
			addtarg(p, b, o, gt.tags, `Gen gt)
		;;
	;;
}

/*
myrtarget: name '=' inputlist ';;'
	| name attrlist = inputlist ';;'
*/
const myrtarget = {b, p, targ
	var ldscript, runtime, install, incpath, tags
	var libdeps, name, inputs, attrs
	var istest, isbench
	var fsel

	match word(p)
	| `std.Some n:	name = n
	| `std.None:	failparse(p, "expected target name after '{}'\n", targ)
	;;

	match attrlist(p)
	| `std.Some al:	attrs = al
	| `std.None:	failparse(p, "invalid attr list for {} {}\n", targ, name)
	;;

	skipspace(p)
	if !matchc(p, '=')
		failparse(p, "expected '=' after '{} {}'\n", targ, name)
	;;

	fsel = mksyssel(b, p.fname, p.line, name)
	match inputlist(p)
	| `std.Some (wl, libs): 
		libdeps = libs
		for w : wl
			sysseladd(fsel, w)
		;;
		inputs = sysselfin(fsel)
		std.slfree(wl)
	| `std.None: failparse(p, "expected list of file names after '{} {}'\n", targ, name)
	;;

	skipspace(p)
	if !matchc(p, ';') || !matchc(p, ';')
		failparse(p, "expected ';;' terminating input list, got {}\n", peekc(p))
	;;

	install = true
	ldscript = ""
	runtime = ""
	incpath = [][:]
	tags = [][:]
	istest = false
	isbench = false
	for elt : attrs
		match elt
		| ("ldscript", lds):	ldscript = std.sldup(lds)
		| ("runtime", rt):	runtime = std.sldup(rt)
		| ("inc", path):	std.slpush(&incpath, std.sldup(path))
		| ("tag", tag):		std.slpush(&tags, tag)
		| ("inst", ""):		install = true
		| ("noinst", ""):	install = false
		| ("test", ""):		istest = true
		| ("bench", ""):	isbench = true
		| ("notest", ""):	istest = false
		| (invalid, ""):	std.fatal("{}: invalid attr '{}'\n", targ, invalid)
		| (invalid, attr):	std.fatal("{}: invalid attr '{} = {}'\n", targ, invalid, attr)
		;;
	;;

	std.sljoin(&incpath, bld.opt_incpaths)
	-> std.mk([
		/* target */
		.dir=std.sldup(p.fdir),
		.name=name,
		.inputs=inputs,
		.libdeps=libdeps,
		.islib=false,
		.istest=istest,
		.isbench=isbench,
		.isdyn=false,
		/* attrs */
		.tags=tags,
		.install=install,
		.ldscript=ldscript,
		.runtime=runtime,
		.incpath=incpath,
	])
}

const datatarget = {b, p, targ
	var name, attrs, blobs
	var tags, path

	match word(p)
	| `std.Some n:	name = n
	| `std.None:	failparse(p, "expected target name after '{}'\n", targ)
	;;

	skipspace(p)
	attrs = [][:]

	match attrlist(p)
	| `std.Some al:	attrs = al
	| `std.None:	failparse(p, "invalid attr list for {} {}\n", targ, name)
	;;

	skipspace(p)
	if !matchc(p, '=')
		failparse(p, "expected '=' after '{} {}'\n", targ, name)
	;;

	match wordlist(p)
	| `std.Some wl:	blobs = wl
	| `std.None:	failparse(p, "expected file list after '{} {}'\n", targ, name)
	;;

	skipspace(p)
	if !matchc(p, ';') || !matchc(p, ';')
		failparse(p, "expected ';;' after input list, got {}\n", peekc(p))
	;;

	tags = [][:]
	for elt : attrs
		match elt
		| ("tag", tag):		std.slpush(&tags, tag)
		| ("path", pathdir):	path = pathdir
		| (tag, ""):
			std.fatal("{}: invalid attr '{}'\n", targ, tag)
		| (tag, attr):
			std.fatal("{}: invalid attr '{}={}'\n", targ, tag, attr)
		;;
	;;

	addtarg(p, b, name, tags, `Data std.mk([
		.dir=std.sldup(p.fdir),
		.name=name,
		.tags=tags,
		.path=path,
		.blobs=blobs,
	]))
}

/* anontarget: '=' wordlist ';;' */
const anontarget = {p, targ
	var inputs

	inputs = [][:]
	skipspace(p)
	if !matchc(p, '=')
		failparse(p, "expected '=' after '{}' target\n", targ)
	;;

	match wordlist(p)
	| `std.Some wl:	inputs = wl
	| `std.None:	failparse(p, "expected file list after '{}' target\n", targ)
	;;
	skipspace(p)
	if !matchc(p, ';') || !matchc(p, ';')
		failparse(p, "expected ';;' terminating input list\n")
	;;
	-> inputs
}

/*
attrlist: EMPTY |'{' attrs '}'

attrs	: EMPTY
	| attrs attr

attr	: name
	| name '=' name
*/
const attrlist = {p
	var al

	al = [][:]
	skipspace(p)
	if !matchc(p, '{')
		-> `std.Some al
	;;
	while true
		match word(p)
		| `std.Some k:
			if matchc(p, '=')
				match word(p)
				| `std.Some v:
					std.slpush(&al, (k, v))
				| `std.None:
					failparse(p, "invalid attr in attribute list\n")
				;;
			else
				std.slpush(&al, (k, [][:]))
			;;
			if !matchc(p, ',')
				break
			;;
		| `std.None:	break
		;;
	;;
	if !matchc(p, '}')
		failparse(p, "expected '}' at end of attr list, got '{}'\n", peekc(p))
	;;
	if al.len == 0
		-> `std.None
	else
		-> `std.Some al
	;;
}

/*
inputlist: EMPTY
	| inputlist input

input	: word
	| "lib" word
*/
const inputlist = {p
	var dir, lib, targ
	var wl, libs

	wl = [][:]
	libs = [][:]
	while true
		match word(p)
		| `std.Some "lib":
			match word(p)
			| `std.Some l:
				(dir, lib, targ) = libpath(p, l)
				std.slpush(&libs, (dir, lib, targ))
			| `std.None:
				failparse(p, "expected lib name after 'lib'\n")
			;;
		| `std.Some w:	std.slpush(&wl, w)
		| `std.None:	break
		;;
	;;
	if wl.len == 0
		-> `std.None
	else
		-> `std.Some (wl, libs)
	;;
}

/* wordlist: EMPTY | wordlist word */
const wordlist = {p
	var wl

	wl = [][:]
	while true
		match word(p)
		| `std.Some w:	std.slpush(&wl, w)
		| `std.None:	break
		;;
	;;
	if wl.len == 0
		-> `std.None
	else
		-> `std.Some wl
	;;
}

/* word: /wordchar*/
/* word: /wordchar*/
const word = {p
	var c, n
	var start

	skipspace(p)

	c = peekc(p)
	if c == '"'
		n = 0
		getc(p)
		start = p.rest
		while p.rest.len > 0
			c = peekc(p)
			if c == '"'
				getc(p)
				goto done
			elif c == '\\'
				c = getc(p)
			;;
			getc(p)
			n += std.charlen(c)
		;;
		failparse(p, "input ended within quoted word\n")
	else
		n = 0
		start = p.rest
		while p.rest.len > 0
			c = peekc(p)
			if wordchar(c)
				getc(p)
				n += std.charlen(c)
			else
				break
			;;
		;;
	;;
:done
	if n > 0
		-> `std.Some std.sldup(start[:n])
	else
		-> `std.None
	;;
}

const wordchar = {c
	-> std.isalnum(c) || \
		c == '.' || c == '_' || c == '$' || c == '-' || \
		c == '/' || c == '@' || c == '!' || c == '~' || \
		c == '+' || c == '%' || c == '&' || c == '(' || \
		c == ')' || c == '[' || c == ']' || c == ':'
}

const skipspace = {p
	var c, r

	r = p.rest
	while r.len > 0
		c = peekc(p)
		match c
		| ' ':	getc(p)
		| '\t':	getc(p)
		| '\n':
			getc(p)
			p.line++
		| '#':
			while p.rest.len > 0 && peekc(p) != '\n'
				getc(p)
			;;
		| _:
			break
		;;
	;;
}

const matchc = {p, c
	var chr, s

	/* safe to use at eof: charstep returns (-1, "") */
	(chr, s) = std.charstep(p.rest)
	if c == chr
		p.rest = s
		-> true
	else
		-> false
	;;
}

const peekc = {p
	-> std.decode(p.rest)
}

const getc = {p
	var c, s

	(c, s) = std.charstep(p.rest)
	p.rest = s
	-> c
}

const addtarg = {p, b, name, tags, targ
	var tn

	tn = std.fmt("{}:{}", p.fdir, name)
	sysseladdlist(p.targsel, tn, tags, (tn, targ))
	std.slpush(&p.targs, targ)
}

const libpath = {p, libpath
	var dir, lib, targ

	match std.strrfind(libpath, ":")
	| `std.None:
		dir = std.sldup(p.fdir)
		lib = std.sldup(libpath)
		targ = std.fmt("{}:{}", p.fdir, lib)
	| `std.Some idx:
		if idx == libpath.len
			std.fatal("libdep {} missing library after ':'\n")
		;;
		/* absolute path */
		if std.hasprefix(libpath, "@/") || std.hasprefix(libpath, "@:")
			dir = std.sldup(libpath[2:idx])
			lib = std.sldup(libpath[idx+1:])
			targ = std.sldup(libpath[2:])
		/* relative path */
		else
			dir = std.pathcat(p.fdir, libpath[:idx])
			lib = std.sldup(libpath[idx+1:])
			targ = std.pathcat(p.fdir, libpath)
			if std.hasprefix(targ, "../")
				std.fatal("library {} outside of project\n", libpath)
			;;
		;;
	;;
	-> (dir, lib, targ)
}

$noret const failparse = {p, msg, args : ...
	var buf : byte[1024]
	var ap
	var sl

	ap = std.vastart(&args)
	sl = std.bfmtv(buf[:], msg, &ap)
	std.fput(1, "{}:{}: {}", p.fname, p.line, sl)
	std.exit(1)
}