From 40f5c3f3f0574e3185819285049cfeb82bac0764 Mon Sep 17 00:00:00 2001
From: "Roberto E. Vargas Caballero" <k0ga@shike2.net>
Date: Fri, 21 Nov 2025 23:00:34 +0100
Subject: [PATCH] bc: Initial version

This version is almost finished, but it does not implements the return
keyword (it is ignored) and it does not include the default bc library
(the option to load it is silently ignored). It also lack tests.
---
 .gitignore |   2 +
 Makefile   |   8 +-
 bc.y       | 707 +++++++++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 715 insertions(+), 2 deletions(-)
 create mode 100644 bc.y

diff --git a/.gitignore b/.gitignore
index 0060cdd..b2dacb8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,6 +7,8 @@
 /libutil.a
 /proto
 /basename
+/bc
+/bc.c
 /cal
 /cat
 /chgrp
diff --git a/Makefile b/Makefile
index c478a2f..754ab1d 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@
 include config.mk
 
 .SUFFIXES:
-.SUFFIXES: .o .c
+.SUFFIXES: .y .o .c
 
 CPPFLAGS =\
 	-D_DEFAULT_SOURCE \
@@ -94,6 +94,7 @@ LIB = libutf.a libutil.a
 
 BIN =\
 	basename\
+	bc\
 	cal\
 	cat\
 	chgrp\
@@ -220,6 +221,9 @@ $(OBJ) $(BIN): $(HDR)
 .c:
 	$(CC) $(CPPFLAGS) $(CFLAGS) -o $@ $< $(LIB)
 
+bc: bc.c
+	$(CC) $(LDFLAGS) -o $@ bc.c $(LIB)
+
 $(MAKEOBJ): make/make.h
 
 make/make: $(MAKEOBJ)
@@ -272,7 +276,7 @@ clean:
 	@cd tests && $(MAKE) clean
 	rm -f $(BIN) $(OBJ) $(LIB) sbase-box sbase-$(VERSION).tar.gz
 	rm -f scripts/make
-	rm -f getconf.h
+	rm -f getconf.h bc.c
 	rm -f proto
 	rm -rf build
 
diff --git a/bc.y b/bc.y
new file mode 100644
index 0000000..ef6d4a2
--- /dev/null
+++ b/bc.y
@@ -0,0 +1,707 @@
+%{
+#include <unistd.h>
+
+#include <ctype.h>
+#include <errno.h>
+#include <setjmp.h>
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "arg.h"
+
+#define DIGITS   "0123456789ABCDEF"
+
+int yydebug;
+
+typedef struct macro Macro;
+
+struct macro {
+	char *init;
+	char *cmp;
+	char *inc;
+	char *vars;
+	char *body;
+	int n;
+};
+
+static int yyerror(char *);
+static int yylex(void);
+
+static void quit(void);
+static char *code(char *, ...);
+static char *forcode(Macro, char *);
+static char *whilecode(Macro, char *);
+static char *ifcode(Macro, char *);
+static char *funcode(Macro, char *, char *);
+static Macro macro(char *, char *, char *);
+static Macro function(char *, char *);
+
+static char *ftn(char *);
+static char *var(char *);
+static char *ary(char *);
+static void writeout(char *);
+
+static size_t used;
+static char yytext[BUFSIZ], buff[BUFSIZ];
+static int lineno, nerr;
+static jmp_buf recover;
+static int nested;
+int cflag, dflag, lflag, sflag;
+
+%}
+
+%union {
+	char *str;
+	char id[2];
+	Macro macro;
+}
+
+%token <id> ID
+%token <str> STRING NUMBER
+%token <str> EQOP '+' '-' '*' '/' '%' '^' INCDEC
+%token EOFTOK
+%token EQ
+%token LE
+%token GE
+%token NE
+%token DEF
+%token BREAK
+%token QUIT
+%token LENGTH
+%token RETURN
+%token FOR
+%token IF
+%token WHILE
+%token SQRT
+%token SCALE
+%token IBASE
+%token OBASE
+%token AUTO
+
+%type <str> assign nexpr expr exprstat rel stat ary statlst
+%type <str> autolst arglst
+%type <str> parlst locals local
+%type <str> function
+%type <macro> fordef cond funbody
+
+%right	'=' EQOP
+%left	'+' '-'
+%left	'*' '/' '%'
+%right	'^'
+
+%start    program
+
+%%
+
+program  : EOFTOK               {quit();}
+         | item program
+         ;
+
+item     : scolonlst '\n'       {used = 0;}
+         | function             {writeout($1); used = 0;}
+         ;
+
+scolonlst: /* empty */
+         | stat                 {writeout($1);}
+         | scolonlst ';' stat   {writeout($3);}
+         | scolonlst ';'
+         ;
+
+statlst : /* empty */           {$$ = code("");}
+        | stat
+        | statlst '\n' stat     {$$ = code("%s%s", $1, $3);}
+        | statlst ';' stat      {$$ = code("%s%s", $1, $3);}
+        | statlst '\n' 
+        | statlst ';'
+        ;
+
+stat    : exprstat
+        | STRING                {$$ = code("[%s]P", $1);}
+        | BREAK                 {$$ = code(" %dQ", nested);}
+        | QUIT                  {quit();}
+        | RETURN
+        | RETURN '(' expr ')'
+        | RETURN '(' ')'
+        | FOR fordef stat       {$$ = forcode($2, $3);}
+        | IF cond stat          {$$ = ifcode($2, $3);}
+        | WHILE cond stat       {$$ = whilecode($2, $3);}
+        | '{' statlst '}'       {$$ = $2;}
+        ;
+
+fordef  : '(' expr ';' rel ';' expr ')' {$$ = macro($2, $4, $6);}
+        ;
+
+cond    : '(' rel ')'           {$$ = macro(NULL, $2, NULL);}
+        ;
+
+function: DEF ID parlst funbody {$$ = funcode($4, $2, $3);}
+        ;
+
+funbody : '{' '\n' autolst statlst '}' {$$ = function($3, $4);}
+        ;
+
+parlst  : '(' ')'               {$$ = "%s";}
+        | '(' locals ')'        {$$ = $2;}
+        ;
+
+autolst : /* empty */           {$$ = "%s";}
+        | AUTO locals '\n'      {$$ = $2;}
+        | AUTO locals ';'       {$$ = $2;}
+        ;
+
+locals  : local
+        | locals ',' local      {$$ = code($1, $3);}
+        ;
+
+local   : ID                    {$$ = code("S%s%%sL%ss.", var($1), var($1));}
+        | ID '[' ']'            {$$ = code("S%s%%sL%ss.", ary($1), ary($1));}
+        ;
+
+arglst  : expr
+        | ID '[' ']'            {$$ = code("%s", ary($1));}
+        | expr ',' arglst       {$$ = code("%s%s", $1, $3);}
+        | ID '[' ']' ',' arglst {$$ = code("%s%s", ary($1), $5);}
+        ;
+
+rel     : expr
+        | expr EQ expr          {$$ = code("%s%s=", $3, $1);}
+        | expr LE expr          {$$ = code("%s%s!=", $3, $1);}
+        | expr GE expr          {$$ = code("%s%s!<", $3, $1);}
+        | expr NE expr          {$$ = code("%s%s!=", $3, $1);}
+        | expr '<' expr         {$$ = code("%s%s<", $3, $1);}
+        | expr '>' expr         {$$ = code("%s%s>", $3, $1);}
+        ;
+
+exprstat: nexpr                 {$$ = code("%s%s", $1, sflag ? "s." : "p");}
+        | assign                {$$ = code("%ss.", $1);}
+        ;
+
+expr    : nexpr
+        | assign
+        ;
+
+nexpr   : NUMBER                {$$ = code(" %s", $1);}
+        | ID                    {$$ = code("l%s", var($1));}
+        | SCALE                 {$$ = code("K");}
+        | IBASE                 {$$ = code("I");}
+        | OBASE                 {$$ = code("O");}
+        | ID ary                {$$ = code("%s;%s", $2, ary($1));}
+        | '(' expr ')'          {$$ = $2;}
+        | ID '(' arglst ')'     {$$ = code("%sl%sx", $3, ftn($1));}
+        | ID '(' ')'            {$$ = code("l%sx", ftn($1));}
+        | '-' expr              {$$ = code("0%s-", $2);}
+        | expr '+' expr         {$$ = code("%s%s+", $1, $3);}
+        | expr '-' expr         {$$ = code("%s%s-", $1, $3);}
+        | expr '*' expr         {$$ = code("%s%s*", $1, $3);}
+        | expr '/' expr         {$$ = code("%s%s/", $1, $3);}
+        | expr '%' expr         {$$ = code("%s%s%", $1, $3);}
+        | expr '^' expr         {$$ = code("%s%s^", $1, $3);}
+        | LENGTH '(' expr ')'   {$$ = code("%sZ", $3);}
+        | SQRT '(' expr ')'     {$$ = code("%sv", $3);}
+        | SCALE '(' expr ')'    {$$ = code("%sX", $3);}
+        | INCDEC ID             {$$ = code("l%s1%sds%s", var($2), $1, var($2));}
+        | INCDEC SCALE          {$$ = code("K1%sk", $1);}
+        | INCDEC IBASE          {$$ = code("I1%sdi", $1);}
+        | INCDEC OBASE          {$$ = code("O1%sdo", $1);}
+        | INCDEC ID ary         {$$ = code("%sdS_;%s1%sdL_:%s", $3, ary($2), $1, ary($2));}
+        | ID INCDEC             {$$ = code("l%sd1%ss%s", var($1), $2, var($1));}
+        | SCALE INCDEC          {$$ = code("Kd1%sk", $2);}
+        | IBASE INCDEC          {$$ = code("Id1%si", $2);}
+        | OBASE INCDEC          {$$ = code("Od1%so", $2);}
+        | ID ary INCDEC         {$$ = code("%sds.;%sd1%sl.:%s", $2, ary($1), $3, ary($1));}
+        ;
+
+assign  : ID '=' expr           {$$ = code("%sds%s", $3, var($1));}
+        | SCALE '=' expr        {$$ = code("%sdk", $3);}
+        | IBASE '=' expr        {$$ = code("%sdi", $3);}
+        | OBASE '=' expr        {$$ = code("%sdo", $3);}
+        | ID ary '=' expr       {$$ = code("%sd%s:%s", $4, $2, ary($1));}
+        | ID EQOP expr          {$$ = code("%sl%s%sds%s", $3, var($1), $2, var($1));}
+        | SCALE EQOP expr       {$$ = code("%sK%sdk", $3, $2);}
+        | IBASE EQOP expr       {$$ = code("%sI%sdi", $3, $2);}
+        | OBASE EQOP expr       {$$ = code("%sO%sdo", $3, $2);}
+        | ID ary EQOP expr      {$$ = code("%s%sds.;%s%sdl.:s", $4, $2, ary($1), $3, ary($1));}
+        ;
+
+ary     : '[' expr ']'          {$$ = $2;}
+        ;
+
+%%
+static int
+yyerror(char *s)
+{
+	fprintf(stderr, "bc: %s:%d:%s\n", "<stdin>", lineno, s);
+	nerr++;
+	longjmp(recover, 1);
+}
+
+static void
+writeout(char *s)
+{
+	if (write(1, s, strlen(s)) < 0)
+		goto err;
+	if (write(1, (char[]){'\n'}, 1) < 0)
+		goto err;
+	return;
+	
+err:
+	perror("bc:writing to dc");
+	exit(1);
+}
+
+static char *
+code(char *fmt, ...)
+{
+	char *s;
+	va_list va;
+	size_t n, room;
+
+	va_start(va, fmt);
+	room = sizeof(buff) - used;
+	n = vsnprintf(buff+used, room, fmt, va);
+	va_end(va);
+
+	if (n < 0 || n >= room) {
+		fprintf(stderr, "bc: unable to code requested operation\n");
+		exit(1);
+	}
+
+	s = buff + used;
+	used += n + 1;
+
+	return s;
+}
+
+static char *
+funcode(Macro d, char *id, char *params)
+{
+	char *s;
+
+	s = code(d.vars, d.body);
+	s = code(params, s);
+	s = code("[%s]s%s", s, id);
+	return s;
+}
+
+static char *
+forcode(Macro d, char *body)
+{
+	char *s;
+
+	s = code("[%s%ss.%s%d]s%d",
+	         body,
+	         d.inc,
+	         d.cmp,
+	         d.n, d.n);
+	writeout(s);
+
+	s = code("%ss.l%dx", d.init, d.n);
+	nested--;
+
+	return s;
+}
+
+static char *
+whilecode(Macro d, char *body)
+{
+	char *s;
+
+	s = code("[%ss.%s%d]s%d", body, d.cmp, d.n, d.n);
+	writeout(s);
+
+	s = code("l%dx", d.n);
+	nested--;
+
+	return s;
+}
+
+static char *
+ifcode(Macro d, char *body)
+{
+	char *s;
+
+	s = code("[%s]s%d", body, d.n);
+	writeout(s);
+
+	s = code("%s%d", d.cmp, d.n);
+	nested--;
+
+	return s;
+}
+
+static Macro
+macro(char *init, char *cmp, char *inc)
+{
+	Macro d;
+
+	if (nested == 10)
+		yyerror("bc:too much nesting");
+
+	d.init = init;
+	d.cmp = cmp;
+	d.inc = inc;
+	d.n = nested++;
+
+	return d;
+}
+
+static Macro
+function(char *vars, char *body)
+{
+	Macro d;
+
+	if (nested == 10)
+		yyerror("bc:too much nesting");
+
+	d.vars = vars;
+	d.body = body;
+	d.n = nested++;
+
+	return d;
+}
+
+static char *
+ary(char *s)
+{
+	return code("%c", toupper(s[0]));
+}
+
+static char *
+ftn(char *s)
+{
+	return code("%c", s[0] | 0x80);
+}
+
+static char *
+var(char *s)
+{
+	return code("%s", s);
+}
+
+static void
+quit(void)
+{
+	exit(nerr > 0 ? 1 : 0);
+}
+
+static void
+skipspaces(void)
+{
+	int ch;
+
+	while (isspace(ch = getchar())) {
+		if (ch == '\n') {
+			lineno++;
+			break;
+		}
+	}
+	ungetc(ch, stdin);
+}
+
+static int
+iden(int ch)
+{
+	static struct keyword {
+		char *str;
+		int token;
+	} keywords[] = {
+		{"define", DEF},
+		{"break", BREAK},
+		{"quit", QUIT},
+		{"length", LENGTH},
+		{"return", RETURN},
+		{"for", FOR},
+		{"if", IF},
+		{"while", WHILE},
+		{"sqrt", SQRT},
+		{"scale", SCALE},
+		{"ibase", IBASE},
+		{"obase", OBASE},
+		{"auto", AUTO},
+		{NULL}
+	};
+	struct keyword *p;
+	char *bp;
+
+	ungetc(ch, stdin);
+	for (bp = yytext; bp < &yytext[sizeof(yytext)]; ++bp) {
+		ch = getchar();
+		if (!islower(ch))
+			break;
+		*bp = ch;
+	}
+
+	if (bp == &yytext[sizeof(yytext)])
+		yyerror("too long token");
+	*bp = '\0';
+	ungetc(ch, stdin);
+
+	if (strlen(yytext) == 1) {
+		strcpy(yylval.id, yytext);
+		return ID;
+	}
+
+	for (p = keywords; p->str && strcmp(p->str, yytext); ++p)
+		;
+
+	if (!p->str)
+		yyerror("invalid keyword");
+
+	return p->token;
+}
+
+static char *
+digits(char *bp)
+{
+	int ch;
+	char *digits = DIGITS, *p;
+
+	while (bp < &yytext[sizeof(yytext)]) {
+		ch = getchar();
+		p = strchr(digits, ch);
+		if (!p)
+			break;
+		*bp++ = ch;
+	}
+
+	if (bp == &yytext[sizeof(yytext)])
+		return NULL;
+	ungetc(ch, stdin);
+
+	return bp;
+}
+
+static int
+number(int ch)
+{
+	int d;
+	char *bp;
+
+	ungetc(ch, stdin);
+	if ((bp = digits(yytext)) == NULL)
+		goto toolong;
+
+	if ((ch = getchar()) != '.') {
+		ungetc(ch, stdin);
+		goto end;
+	}
+	*bp++ = '.';
+
+	if ((bp = digits(bp)) == NULL)
+		goto toolong;
+
+end:
+	if (bp ==  &yytext[sizeof(yytext)])
+		goto toolong;
+	*bp = '\0';
+	yylval.str = yytext;
+
+	return NUMBER;
+
+toolong:
+	yyerror("bc:too long number");
+}
+
+static int
+string(int ch)
+{
+	char *bp;
+
+	for (bp = yytext; bp < &yytext[sizeof(yytext)]; ++bp) {
+		if ((ch = getchar()) == '"')
+			break;
+		*bp = ch;
+	}
+
+	if (bp == &yytext[sizeof(yytext)])
+		yyerror("bc:too long string");
+	*bp = '\0';
+	yylval.str = yytext;
+
+	return STRING;
+}
+
+static int
+follow(int next, int yes, int no)
+{
+	int ch;
+
+	ch = getchar();
+	if (ch == next)
+		return yes;
+	ungetc(ch, stdin);
+	return no;
+}
+
+static int
+operand(int ch)
+{
+	switch (ch) {
+	case '\n':
+	case '{':
+	case '}':
+	case '[':
+	case ']':
+	case '(':
+	case ')':
+	case ',':
+	case ';':
+		return ch;
+	case '"':
+		return string(ch);
+	case '*':
+		yylval.str = "*";
+		return follow('=', EQOP, '*');
+	case '/':
+		yylval.str = "/";
+		return follow('=', EQOP, '/');
+	case '%':
+		yylval.str = "%";
+		return follow('=', EQOP, '%');
+	case '=':
+		return follow('=', EQ, '=');
+	case '+':
+	case '-':
+		yylval.str = (ch == '+') ? "+" : "-";
+		if (follow('=', EQOP, ch) != ch)
+			return EQOP;
+		return follow(ch, INCDEC, ch);
+	case '^':
+		yylval.str = "^";
+		return follow('=', EQOP, '^');
+	case '<':
+		return follow('=', LE, '<');
+	case '>':
+		return follow('=', GE, '>');
+	case '!':
+		if (getchar() == '=')
+			return NE;
+	default:
+		yyerror("invalid operand");
+	}
+}
+
+static void
+comment(void)
+{
+	do {
+		while (getchar() != '*')
+			;
+	} while (getchar() != '/');
+}
+
+static int
+yylex(void)
+{
+	int peekc, ch;
+
+repeat:
+	skipspaces();
+
+	ch = getchar();
+	if (ch == EOF) {
+		return EOFTOK;
+	} else if (!isascii(ch)) {
+		yyerror("invalid input character");
+	} else if (islower(ch)) {
+		return iden(ch);
+	} else if (strchr(DIGITS, ch)) {
+		return number(ch);
+	} else {
+		if (ch == '/') {
+			peekc = getchar();
+			if (peekc == '*') {
+				comment();
+				goto repeat;
+			}
+			ungetc(peekc, stdin);
+		}
+		return operand(ch);
+	}
+}
+
+static void
+spawn(void)
+{
+	int fds[2];
+	char errmsg[] = "bc:error execing dc\n";
+
+	if (pipe(fds) < 0) {
+		perror("bc:creating pipe");
+		exit(1);
+	}
+
+	switch (fork()) {
+	case -1:
+		perror("bc:forking dc");
+		exit(1);
+	case 0:
+		close(1);
+		dup(fds[1]);
+		close(fds[0]);
+		close(fds[1]);
+		break;
+	default:
+		close(0);
+		dup(fds[0]);
+		close(fds[0]);
+		close(fds[1]);
+		execlp("dc", "dc", (char *) NULL);
+
+		/* it shouldn't happen */
+		write(3, errmsg, sizeof(errmsg)-1);
+		_Exit(2);
+	}
+}
+
+static void
+init(void)
+{
+}
+
+static int
+run(void)
+{
+	if (setjmp(recover))
+		return 1;
+
+	yyparse();
+	return 1;
+}
+
+static void
+usage(void)
+{
+	fputs("usage: bc [-cdls]\n", stderr);
+	exit(1);
+}
+
+int
+main(int argc, char *argv[])
+{
+	ARGBEGIN {
+	case 'c':
+		cflag = 1;
+		break;
+	case 'd':
+		dflag = 1;
+		yydebug = 3;
+		break;
+	case 'l':
+		lflag = 1;
+		break;
+	case 's':
+		sflag = 1;
+		break;
+	default:
+		usage();
+	} ARGEND
+
+	if (!cflag)
+		spawn();
+
+	for (init(); run(); init())
+		;
+}
-- 
2.45.4

