/* YAX (Yet Another Instruction Code Set) Interpreter v1.0
   simple procedural/functional language with lisp-lookalike syntax.
   eats sources with extension .yax for dinner.               */

OPT STACK=25000     /* we do heavy recursion */

OBJECT var          /* this is where we store our runtime values */
  type:INT
  name:LONG
  value:LONG
ENDOBJECT

/* intermediate codes */
ENUM ENDSOURCE,VALUE,ISTRING,IDENT,LBRACKET,RBRACKET

/* keywords */
ENUM FWRITE=100,FADD,FEQ,FUNEQ,FSUB,FMUL,FDIV,FAND,FORX,FNOT,FIF,FDO,
     FSELECT,FSET,FFOR,FWHILE,FUNTIL,FDEFUN,FLAMBDA,FAPPLY,FREADINT,
     FARRAY,FGREATER,FSMALLER,FLOCATE,FCLS,FDUMP,FWINDOW,FTELL,FTOLD,
     FSEE,FSEEN,FSTRING,FREAD,FGET,FPUT,FFILELEN,FLINE,FPLOT,FBOX,
     FMOUSEX,FMOUSEY,FMOUSE,FTEXT,LAST

CONST KEYWORDSIZE=8,
      NRKEYWORDS=LAST-99,
      IDENTNAMESPACE=30000,
      VARSTACKSPACE=50000,
      MAXARGS=5,
      ERLEN=60

/* errors */
ENUM ER_WORKSPACE=1,ER_BUF,ER_GARBAGE,ER_SYNTAX,ER_EXPKEYWORD,ER_EXPRBRACKET,
     ER_EXPEXP,ER_QUOTE,ER_COMMENT,ER_INFILE,ER_SOURCEMEM,ER_EXPIDENT,
     ER_ARGS,ER_TYPE,ER_EXPLBRACKET,ER_STACK,ER_ALLOC,ER_ARRAY,ER_FILE,
     ER_GFXWIN,ER_VALUES

/* variable types */
ENUM TINTEGER=1,TSTRING,TFUNC,TARRAY

DEF source,slen,erpos=NIL,
    ilen,ibuf,ipos:PTR TO INT,p:PTR TO INT,idents,
    name[100]:STRING,wfile,
    inputbuf[100]:STRING,winspec[100]:STRING,
    vartop,varbottom,vars,rec,globvar,
    infile,outfile,oldout,oldin,stdin,gfxwindow=0

PROC main()
  WriteF(''); stdin:=stdout
  loadsource()
  ilen:=Mul(slen,4)+1000       /* guess the needed workspace */
  ibuf:=New(ilen+10)
  idents:=String(IDENTNAMESPACE)
  vars:=New(VARSTACKSPACE)
  vartop:=vars; varbottom:=vars
  IF (ibuf=NIL) OR (idents=NIL) OR (vars=NIL)
    error(ER_WORKSPACE)
  ELSE 
    lexanalyse()               /* translate to intermediate format */
    p:=ibuf
    WHILE p[]<>ENDSOURCE DO eval()       /* run the code */
  ENDIF
  error(0)
ENDPROC

PROC lexanalyse()
  DEF pos,end,c,count,ident[50]:STRING,pos2,keypos,a,nr,ident2[50]:STRING
  pos:=source; end:=pos+slen; ipos:=ibuf; erpos:=pos
  StrCopy(idents,' ',1)
  loop:
  c:=pos[]++
  IF c>96                          /* an identifier */
    pos2:=pos-1
    WHILE pos[]++>96 DO NOP; DEC pos
    StrCopy(ident,pos2,pos-pos2)
    StrCopy(ident2,ident,ALL)
    StrAdd(ident,'..............',ALL)
    keypos:={keywords}
    nr:=0
    FOR a:=1 TO NRKEYWORDS         /* lookup keywords */
      IF StrCmp(ident,keypos,KEYWORDSIZE)
        nr:=99+a
        JUMP found
      ENDIF
      keypos:=keypos+KEYWORDSIZE
    ENDFOR
    found:
    IF nr>0                        /* keyword */
      iword(nr)
    ELSE                           /* own identifier */
      iword(IDENT)
      StrCopy(ident,' ',1)
      StrAdd(ident,ident2,ALL)
      StrAdd(ident,' ',1)
      pos2:=InStr(idents,ident,0)
      IF pos2=-1
        ilong(EstrLen(idents)+idents)
        StrAdd(idents,ident2,ALL)
        StrAdd(idents,' ',1)
        IF EstrLen(idents)=StrMax(idents) THEN error(ER_WORKSPACE)
      ELSE
        ilong(pos2+idents+1)
      ENDIF
    ENDIF
  ELSE
    SELECT c                       /* anything else */
      CASE " "
        IF pos<end THEN JUMP loop
      CASE "("
        iword(LBRACKET)
        erpos:=pos-1
        ilong(erpos)
      CASE ")"; iword(RBRACKET)
      CASE "+"; iword(FADD)
      CASE "-"
        IF pos[]=" "
          iword(FSUB)
        ELSE
          iword(VALUE)
          ilong(-Val(pos,{c}))
          IF c=0 THEN error(ER_GARBAGE) ELSE pos:=pos+c
        ENDIF
      CASE "*"; iword(FMUL)
      CASE "/"
        IF pos[]<>"*"
          iword(FDIV)
        ELSE                       /* comment (like this one) */
          INC pos
          WHILE pos-1<end
            INC count
            IF (pos[]++="*") AND (pos[]="/") THEN JUMP out
          ENDWHILE
          error(ER_COMMENT)
          out:
          INC pos
        ENDIF
      CASE "="
        iword(FEQ)
      CASE "?"
        iword(FUNEQ)
      CASE "'"                     /* string constant */
        iword(ISTRING)
        count:=0; pos2:=pos
        WHILE pos[]++<>"'"
          INC count
          IF pos=end THEN error(ER_QUOTE)
        ENDWHILE
        iword(count)
        ilong(pos2)                /* char adress */
      CASE 10
        IF pos<end THEN JUMP loop
      CASE 0
        pos:=end
      CASE 9
        IF pos<end THEN JUMP loop
      DEFAULT
        iword(VALUE)
        ilong(Val(pos--,{c}))
        IF c=0 THEN error(ER_GARBAGE) ELSE pos:=pos+c
    ENDSELECT
  ENDIF
  IF pos<end THEN JUMP loop
  iword(ENDSOURCE)
ENDPROC

PROC checkstop()
  IF FreeStack()<1000 THEN error(ER_STACK)
  IF CtrlC() THEN error(-1)
ENDPROC

PROC eval()                        /* main recursive evaluation function */
  DEF r=0,i,ins,p2,x:PTR TO LONG,a,adr:PTR TO var
  checkstop()
  i:=p[]++
  SELECT i
    CASE VALUE
      r:=^p++
    CASE IDENT
      r:=varvalue(^p++,TINTEGER)
    CASE LBRACKET
      erpos:=^p++
      ins:=p[]++
      IF ins=IDENT
        adr:=findvar(^p++)
        IF adr.type=TFUNC
          r:=dofunc(adr.value)
        ELSE
          IF adr.type<>TARRAY THEN error(ER_TYPE)
          x:=adr.value
          a:=eval()
          IF (a<0) OR (a>x[]) THEN error(ER_ARRAY)
          r:=x[a+1]
        ENDIF
      ELSE
        IF ins<100 THEN error(ER_EXPKEYWORD)
        SELECT ins
          CASE FWRITE                /* output string constants + expressions */
            x:=TRUE
            WHILE p[]<>RBRACKET
              IF p[]=ISTRING
                Write(stdout,Long(p+4),p[1])
                IF (p[1]=0) AND (p[4]=RBRACKET) THEN x:=FALSE
                p:=p+8
              ELSEIF p[]=IDENT
                IF (Int(findvar(Long(p+2)))=TSTRING)
                  WriteF('\s',eatstring())
                ELSE
                  WriteF('\d',eval())
                ENDIF
              ELSE
                WriteF('\d',eval())
              ENDIF
            ENDWHILE
            IF x THEN WriteF('\n')
          CASE FEQ
            r:=TRUE
            x:=eval()
            WHILE p[]<>RBRACKET DO IF x<>eval() THEN r:=FALSE
          CASE FUNEQ; r:=eval()<>eval()
          CASE FGREATER; r:=eval()>eval()
          CASE FSMALLER; r:=eval()<eval()
          CASE FADD; r:=eval(); WHILE p[]<>RBRACKET DO r:=r+eval()
          CASE FSUB; r:=eval(); WHILE p[]<>RBRACKET DO r:=r-eval()
          CASE FMUL; r:=eval(); WHILE p[]<>RBRACKET DO r:=Mul(r,eval())
          CASE FDIV; r:=eval(); WHILE p[]<>RBRACKET DO r:=Div(r,eval())
          CASE FAND; r:=eval(); WHILE p[]<>RBRACKET DO r:=r AND eval()
          CASE FORX; r:=eval(); WHILE p[]<>RBRACKET DO r:=r OR eval()
          CASE FNOT; r:=Not(eval())
          CASE FIF
            IF eval()
              r:=eval()
              IF p[]<>RBRACKET THEN skip()
            ELSE
              skip()
              IF p[]<>RBRACKET THEN r:=eval()
            ENDIF
          CASE FDO; WHILE p[]<>RBRACKET DO r:=eval()
          CASE FSELECT
            x:=eval()
            WHILE p[]<>RBRACKET DO IF x=eval() THEN r:=eval() ELSE skip()
          CASE FSET
            IF p[]=LBRACKET
              p:=p+2
              erpos:=^p++
              x:=varvalue(eatident(),TARRAY)
              a:=eval()
              IF (a<0) OR (a>x[0]) THEN error(ER_ARRAY)
              IF p[]++<>RBRACKET THEN error(ER_EXPRBRACKET)
              x[a+1]:=eval()
            ELSE
              x:=eatident()
              IF (p[]=LBRACKET) AND (p[3]=FLAMBDA)
                p:=p+8
                adr:=findvar(x)
                letvar(adr,p,TFUNC)
                WHILE p[]<>RBRACKET DO skip()
                p:=p+2
              ELSE
                r:=eval()
                x:=findvar(x)
                letvar(x,r,TINTEGER)
              ENDIF
            ENDIF
          CASE FFOR
            x:=eatident()
            r:=eval()
            adr:=findvar(x)
            x:=eval()
            p2:=p
            IF r>x               /* downto */
              FOR a:=r TO x STEP -1
                p:=p2
                letvar(adr,a,TINTEGER)
                WHILE p[]<>RBRACKET DO eval()
              ENDFOR
            ELSE
              FOR a:=r TO x
                p:=p2
                letvar(adr,a,TINTEGER)
                WHILE p[]<>RBRACKET DO eval()
              ENDFOR
            ENDIF
            r:=0
          CASE FWHILE
            p2:=p
            WHILE eval()
              WHILE p[]<>RBRACKET DO eval()
              p:=p2
            ENDWHILE
            WHILE p[]<>RBRACKET DO skip()
            r:=0
          CASE FUNTIL
            p2:=p
            WHILE eval()=FALSE
              WHILE p[]<>RBRACKET DO eval()
              p:=p2
            ENDWHILE
            WHILE p[]<>RBRACKET DO skip()
            r:=0
          CASE FDEFUN
            x:=eatident()
            adr:=findvar(x)
            letvar(adr,p,TFUNC)
            WHILE p[]<>RBRACKET DO skip()
          CASE FLAMBDA; error(ER_SYNTAX)
          CASE FAPPLY
            IF p[]<>IDENT
              IF (p[]<>LBRACKET) OR (p[3]<>FLAMBDA) THEN error(ER_EXPIDENT)
              p:=p+8; adr:=p
              WHILE p[]<>RBRACKET DO skip()
              p:=p+2
              r:=dofunc(adr)
            ELSE
              p:=p+2
              r:=dofunc(varvalue(^p++,TFUNC))
            ENDIF
          CASE FREADINT
            IF ReadStr(stdin,inputbuf)=-1
              r:=0
            ELSE
              r:=Val(inputbuf,{x})
            ENDIF
          CASE FARRAY
            adr:=findvar(eatident())
            a:=eval()
            x:=New(Mul(a,4)+8)
            IF x=NIL THEN error(ER_ALLOC)
            letvar(adr,x,TARRAY)
            x[0]:=a
          CASE FLOCATE; WriteF('\e[\d;\dH',eval(),eval())
          CASE FCLS; Out(stdout,12)
          CASE FDUMP
            adr:=varbottom
            WriteF('\n')
            WHILE adr<vartop
              a:=adr.name
              x:=a
              WHILE Char(x)<>" " DO INC x
              Write(stdout,a,x-a)
              x:=adr.type
              SELECT x
                CASE TINTEGER; WriteF(' = \d (int)\n',adr.value)
                CASE TSTRING;  WriteF(' = "\s" (string)\n',adr.value)
                CASE TFUNC;    WriteF(' (function)\n')
                CASE TARRAY;   WriteF('[\d] (array)\n',Long(adr.value))
              ENDSELECT
              adr:=adr+SIZEOF var
            ENDWHILE
            WriteF('\n')
          CASE FWINDOW
            StringF(winspec,'CON:\d/\d/\d/\d/',eval(),eval(),eval(),eval())
            x:=eatstring()
            StrAdd(winspec,x,ALL)
            wfile:=Open(winspec,1006)
            IF wfile=NIL THEN error(ER_FILE)
            IF conout<>NIL THEN Close(conout)
            stdout:=wfile
            conout:=stdout
            stdin:=stdout
            adr:=OpenWorkBench()
            Forbid()
            a:=NIL
            IF adr<>NIL
              adr:=Long(adr+4)
              WHILE (adr<>NIL) AND (a=NIL)
                IF StrCmp(x,Long(adr+32),ALL) THEN a:=adr
                adr:=^adr
              ENDWHILE
            ENDIF
            Permit()
            IF a THEN gfxwindow:=a
          CASE FTELL
            IF outfile<>NIL THEN Close(outfile)
            outfile:=NIL
            outfile:=Open(eatstring(),1006)
            IF outfile=NIL THEN error(ER_FILE)
            oldout:=stdout
            stdout:=outfile
          CASE FTOLD
            IF outfile<>NIL THEN Close(outfile)
            outfile:=NIL
            stdout:=oldout
          CASE FSEE
            IF infile<>NIL THEN Close(infile)
            infile:=NIL
            infile:=Open(eatstring(),1005)
            IF infile=NIL THEN error(ER_FILE)
            oldin:=stdin
            stdin:=infile
          CASE FSEEN
            IF infile<>NIL THEN Close(infile)
            infile:=NIL
            stdin:=oldin
          CASE FSTRING
            adr:=String(250)
            IF adr=NIL THEN error(ER_ALLOC)
            letvar(findvar(eatident()),adr,TSTRING)
          CASE FREAD
            x:=varvalue(eatident(),TSTRING)
            r:=ReadStr(stdin,x)
          CASE FGET; r:=Inp(stdin)
          CASE FPUT; r:=eval(); IF r<>-1 THEN Out(stdout,r)
          CASE FFILELEN
            r:=FileLength(eatstring())
            IF r=-1 THEN r:=0
          CASE FLINE; getrast(); Line(eval(),eval(),eval(),eval(),eval())
          CASE FPLOT; getrast(); Plot(eval(),eval(),eval())
          CASE FBOX
            getrast()
            a:=eval(); x:=eval(); p2:=eval(); r:=eval()
            IF (a>p2) OR (x>r) THEN error(ER_VALUES)
            Box(a,x,p2,r,eval())
            r:=0
          CASE FMOUSEX; r:=MouseX(getwin())
          CASE FMOUSEY; r:=MouseY(getwin())
          CASE FMOUSE; r:=Mouse()
          CASE FTEXT
            adr:=getrast()
            a:=eval(); x:=eval()
            Colour(eval(),eval())
            TextF(a,x,eatstring())
        ENDSELECT
      ENDIF
      IF p[]++<>RBRACKET THEN error(ER_EXPRBRACKET)
    DEFAULT
      IF (i=RBRACKET) OR (i=ISTRING) THEN error(ER_EXPEXP) ELSE error(ER_SYNTAX)
  ENDSELECT
ENDPROC r

PROC getwin()
  IF gfxwindow=NIL THEN error(ER_GFXWIN)
ENDPROC gfxwindow

PROC getrast()
  DEF r
  IF gfxwindow=NIL THEN error(ER_GFXWIN)
  r:=Long(gfxwindow+50)
  SetStdRast(r)
ENDPROC r

PROC eatstring()
  DEF adr,x
  IF p[]=ISTRING
    p:=p+2; x:=p[]++; adr:=^p++
    adr[x]:=0
  ELSE
    adr:=varvalue(eatident(),TSTRING)
  ENDIF
ENDPROC adr

PROC eatident()
  IF p[]++<>IDENT THEN error(ER_EXPIDENT)
ENDPROC ^p++

PROC dofunc(lcode)
  DEF args[MAXARGS]:ARRAY OF LONG,a=0,oldvarb,oldvart,oldp,x,r=0,olderpos
  checkstop()
  WHILE p[]<>RBRACKET
    IF a=MAXARGS THEN error(ER_ARGS)
    args[a]:=eval()
    INC a
  ENDWHILE
  IF rec=0 THEN globvar:=vartop
  oldvarb:=varbottom; varbottom:=vartop; oldvart:=vartop;
  oldp:=p; p:=lcode; olderpos:=erpos; INC rec
  IF p[]++<>LBRACKET THEN error(ER_EXPLBRACKET)
  erpos:=^p++
  WHILE p[]<>RBRACKET
    IF a=0 THEN error(ER_ARGS)
    x:=findvar(eatident())
    letvar(x,args[]++,TINTEGER)
    DEC a
  ENDWHILE
  IF a<>0 THEN error(ER_ARGS)
  p:=p+2
  WHILE p[]<>RBRACKET DO r:=eval()
  varbottom:=oldvarb; vartop:=oldvart; p:=oldp; erpos:=olderpos; DEC rec
ENDPROC r

PROC findvar(id)
  DEF loc=0:PTR TO var,a:PTR TO var
  IF vartop<>varbottom
    a:=varbottom                     /* check existing local vars */
    WHILE (a<vartop) AND (loc=0)
      IF a.name=id THEN loc:=a
      a:=a+SIZEOF var
    ENDWHILE
  ENDIF
  IF loc=0
    IF (rec>0) AND (globvar>vars)    /* check global vars */
      a:=vars
      WHILE (a<globvar) AND (loc=0)
        IF a.name=id THEN loc:=a
        a:=a+SIZEOF var
      ENDWHILE
    ENDIF
    IF loc=0                         /* create new var dynamically */
      loc:=vartop
      vartop:=vartop+SIZEOF var
      IF vars+VARSTACKSPACE<vartop THEN error(ER_WORKSPACE)
      loc.type:=TINTEGER
      loc.name:=id
      loc.value:=0
    ENDIF
  ENDIF
ENDPROC loc

PROC letvar(adr:PTR TO var,value,type)
  IF (adr.type<>type) AND (adr.type<>TINTEGER) THEN error(ER_TYPE)
  checkstop()
  adr.type:=type
  adr.value:=value
ENDPROC

PROC varvalue(id,type)
  DEF adr:PTR TO var
  checkstop()
  adr:=findvar(id)
  IF adr.type<>type THEN error(ER_TYPE)
ENDPROC adr.value

PROC skip()                        /* skip *one* expression */
  DEF deep=0,i
  REPEAT
    i:=p[]++
    IF (i=VALUE) OR (i=LBRACKET) OR (i=IDENT) THEN p:=p+4
    IF i=ISTRING THEN p:=p+6
    IF i=LBRACKET THEN INC deep
    IF i=RBRACKET THEN IF deep=0 THEN error(ER_EXPEXP) ELSE DEC deep
    IF i=ENDSOURCE THEN error(ER_EXPRBRACKET)
  UNTIL deep=0
ENDPROC

PROC iword(x)
  IF ibuf+ilen>ipos THEN ipos[]++:=x ELSE error(ER_BUF)
ENDPROC

PROC ilong(x)
  IF ibuf+ilen>ipos THEN ^ipos++:=x ELSE error(ER_BUF)
ENDPROC

PROC loadsource()
  DEF suxxes=FALSE,handle,read
  IF StrCmp(arg,'?',ALL) OR StrCmp(arg,'',ALL)
    WriteF('USAGE: Yax <source> (default ext. ".yax")\n')
    error(0)
  ELSE
    StrCopy(name,arg,ALL)
    StrAdd(name,'.yax',4)
    slen:=FileLength(name)
    handle:=Open(name,1005)
    IF (handle=NIL) OR (slen=-1)
      error(ER_INFILE)
    ELSE
      source:=New(slen+10)
      IF source=NIL
        error(ER_SOURCEMEM)
      ELSE
        read:=Read(handle,source,slen)
        Close(handle)
        IF read=slen 
          suxxes:=TRUE
          source[slen]:=0
        ELSE
          error(ER_INFILE)
        ENDIF
      ENDIF
    ENDIF
  ENDIF
ENDPROC

PROC error(nr)
  DEF erstr[ERLEN]:STRING,a
  IF outfile<>NIL
    IF stdout=outfile THEN stdout:=oldout
    Close(outfile)
  ENDIF
  IF infile<>NIL
    IF stdin=infile THEN stdin:=oldin
    Close(infile)
  ENDIF
  WriteF('\n')
  IF nr>0
    WriteF('ERROR: ')
    SELECT nr
      CASE ER_WORKSPACE;   WriteF('Could not allocate workspace!\n')
      CASE ER_BUF;         WriteF('Buffer overflow!\n')
      CASE ER_GARBAGE;     WriteF('Garbage in line\n')
      CASE ER_SYNTAX;      WriteF('Your syntax sucks\n')
      CASE ER_EXPKEYWORD;  WriteF('Keyword identifier expected\n')
      CASE ER_EXPRBRACKET; WriteF('Right bracket expected\n')
      CASE ER_EXPEXP;      WriteF('Evaluateable expression expected\n')
      CASE ER_QUOTE;       WriteF('Missing quote \a\n')
      CASE ER_COMMENT;     WriteF('Missing "*/"\n')
      CASE ER_SOURCEMEM;   WriteF('No Memory for source!\n')
      CASE ER_INFILE;      WriteF('Could not open file "\s".\n',name)
      CASE ER_EXPIDENT;    WriteF('Identifier expected\n')
      CASE ER_ARGS;        WriteF('Illegal #of arguments\n')
      CASE ER_TYPE;        WriteF('Wrong type of variable/expression\n')
      CASE ER_EXPLBRACKET; WriteF('Left bracket expected\n')
      CASE ER_STACK;       WriteF('Nearly stack overflow: \d deep\n',rec)
      CASE ER_ALLOC;       WriteF('Dynamic allocation failed!\n')
      CASE ER_ARRAY;       WriteF('Array index out of bounds\n')
      CASE ER_FILE;        WriteF('File error\n')
      CASE ER_GFXWIN;      WriteF('No User-window for graphics\n')
      CASE ER_VALUES;      WriteF('Illegal value(s)\n')
    ENDSELECT
    IF erpos<>NIL
      StrCopy(erstr,erpos,ALL)
      FOR a:=0 TO ERLEN-1 DO IF erstr[a]=10 THEN erstr[a]:=32
      WriteF('NEARBY: \s\n',erstr)
    ENDIF
  ELSEIF nr=-1
    WriteF('*** Program halted.\n')
  ENDIF
  IF conout<>NIL THEN WriteF('Press <return> to continue ...\n')
  CleanUp(0)
ENDPROC

keywords:
CHAR 'write...', 'add.....', 'eq......', 'uneq....', 'sub.....',
     'mul.....', 'div.....', 'and.....', 'or......', 'not.....',
     'if......', 'do......', 'select..', 'set.....', 'for.....',
     'while...', 'until...', 'defun...', 'lambda..', 'apply...',
     'readint.', 'array...', 'greater.', 'smaller.', 'locate..',
     'cls.....', 'dump....', 'window..', 'tell....', 'told....',
     'see.....', 'seen....', 'string..', 'read....', 'get.....',
     'put.....', 'filelen.', 'line....', 'plot....', 'box.....',
     'mousex..', 'mousey..', 'mouse...', 'text....'
