;;; Gash -- Guile As SHell ;;; Copyright © 2020, 2021 Timothy Sample ;;; ;;; This file is part of Gash. ;;; ;;; Gash is free software: you can redistribute it and/or modify ;;; it under the terms of the GNU General Public License as published by ;;; the Free Software Foundation, either version 3 of the License, or ;;; (at your option) any later version. ;;; ;;; Gash is distributed in the hope that it will be useful, ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ;;; GNU General Public License for more details. ;;; ;;; You should have received a copy of the GNU General Public License ;;; along with Gash. If not, see . (define-module (gash arithmetic) #:use-module (ice-9 i18n) #:use-module (ice-9 match) #:use-module (ice-9 receive) #:use-module (srfi srfi-1) #:use-module (srfi srfi-26) #:use-module (system base lalr) #:export (read-arithmetic)) ;;; Commentary: ;;; ;;; This module contains the lexer and parser for reading arithmetic ;;; expansions. ;;; ;;; Code: (define nonzero-digit? (let ((nonzero-digits (string->char-set "123456789"))) (lambda (chr) "Check if @var{chr} is a nonzero digit." (char-set-contains? nonzero-digits chr)))) (define* (lex-number str #:optional (start 0) (end (string-length str))) "Read a number token from @var{str} and return two values: the token and its length. If a number cannot be read, then the resulting token will be @code{#f} and the length will be zero." (define (->token offset base) (let ((str* (substring str (+ start offset) end))) (receive (n count) (locale-string->integer str* base) (if n (values (make-lexical-token 'NUMBER #f n) (+ offset count)) (values #f 0))))) (match (string-ref str start) (#\0 (match (and (< (1+ start) end) (string-ref str (1+ start))) ((or #\x #\X) (->token 2 16)) (_ (->token 0 8)))) ((? nonzero-digit?) (->token 0 10)) (_ (values #f 0)))) (define *operators* `(("(" . LPAREN) (")" . RPAREN) ("~" . BITNOT) ("!" . LOGNOT) ("*" . *) ("/" . /) ("%" . %) ("+" . +) ("-" . -) ("<<" . <<) (">>" . >>) ("<" . <) ("<=" . <=) (">" . >) (">=" . >=) ("==" . ==) ("!=" . !=) ("&" . BITAND) ("^" . BITXOR) ("|" . BITIOR) ("&&" . LOGAND) ("||" . LOGIOR) ("?" . ?) (":" . :) ("=" . =) ("*=" . *=) ("/=" . /=) ("%=" . %=) ("+=" . +=) ("-=" . -=) ("<<=" . <<=) (">>=" . >>=) ("&=" . BITAND-ASSIGN) ("^=" . BITXOR-ASSIGN) ("|=" . BITIOR-ASSIGN))) (define* (operator-prefix? str #:optional (start 0) (end (string-length str))) "Check if @var{str} is a prefix of an arithmetic operator." (any (cut string-prefix? str <> start end) (map car *operators*))) (define* (lex-operator str #:optional (start 0) (end (string-length str))) "Read an operator token from @var{str} and return two values: the token and its length. If an operator cannot be read, then the resulting token will be @code{#f} and the length will be zero." (define (->token op) (if (string-null? op) (values #f 0) (values (make-lexical-token (assoc-ref *operators* op) #f op) (string-length op)))) (let loop ((k start) (acc "")) (if (< k end) (let ((next (string-append acc (string (string-ref str k))))) (if (operator-prefix? next) (loop (1+ k) next) (->token acc))) (->token acc)))) (define name-start-char? (let ((char-set:name-start (char-set-intersection char-set:ascii (char-set-union char-set:letter (char-set #\_))))) (lambda (chr) "Check if @var{chr} is a valid first character for a name." (and (char? chr) (char-set-contains? char-set:name-start chr))))) (define name-char? (let ((char-set:name (char-set-intersection char-set:ascii (char-set-union char-set:letter+digit (char-set #\_))))) (lambda (chr) "Check if @var{chr} is a valid character for a name." (and (char? chr) (char-set-contains? char-set:name chr))))) (define* (lex-name str #:optional (start 0) (end (string-length str))) "Read a name token from @var{str} and return two values: the token and its length. If a name cannot be read, then the resulting token will be @code{#f} and the length will be zero." (match (string-ref str start) ((? name-start-char? ch) (let loop ((k (1+ start)) (acc (list ch))) (match (and (< k end) (string-ref str k)) ((? name-char? ch) (loop (1+ k) (cons ch acc))) (_ (let ((result (reverse-list->string acc))) (values (make-lexical-token 'NAME #f result) (string-length result))))))) (_ (values #f 0)))) (define (make-lexer str) "Return a lexer that reads tokens from @var{str}. This lexer is a stateful thunk that returns the next token each time it is called. It is suitable to be used with an @code{lalr-parser}." (define %lexers (list lex-number lex-operator lex-name)) (define idx 0) (lambda () (set! idx (string-index str char-set:graphic idx)) (if (and idx (< idx (string-length str))) (let loop ((lexers %lexers)) (match lexers (() (error "could not read arithmetic substitution" str idx)) ((lex . rest) (receive (token count) (lex str idx) (if token (begin (set! idx (+ idx count)) token) (loop rest)))))) '*eoi*))) (define (make-ref name) "Return a Scheme expression that looks up @var{name} in the current Gash environment, returning zero if @var{name} is not set." `(or (string->number (getvar ,name "0")) 0)) (define* (make-assign name expr #:optional make-expr) "Return a Scheme expression that sets @var{name} to the result of @var{expr} in the current Gash environment. Optionally, @var{make-expr} can be used to adjust the result of @var{expr} while setting @var{name}." `(let ((result ,(match make-expr ((? symbol?) `(,make-expr ,(make-ref name) ,expr)) ((? procedure?) (make-expr (make-ref name) expr)) (#f expr)))) (setvar! ,name (number->string result)) result)) (define (make-bool expr) "Return a Scheme expression that converts the Boolean expression @var{expr} into a number (one for true, zero for false)." `(if ,expr 1 0)) (define (nonzero? expr) "Return a Scheme expression that checks if @var{expr} is an expression that returns a nonzero number." `(not (zero? ,expr))) (define (make-parser) "Create a parser that reads arithmetic expansion expressions and returns equivalent Scheme expressions." (lalr-parser (NAME NUMBER LPAREN RPAREN (right: = *= /= %= += -= <<= >>= BITAND-ASSIGN BITXOR-ASSIGN BITIOR-ASSIGN) (right: ? :) (left: LOGIOR) (left: LOGAND) (left: BITIOR) (left: BITXOR) (left: BITAND) (left: == !=) (left: < <= > >=) (left: << >>) (left: + -) (left: * / %) (nonassoc: LOGNOT) (nonassoc: BITNOT) (nonassoc: unary-) (nonassoc: unary+)) (expr (NAME) : (make-ref $1) (NUMBER) : $1 (LPAREN expr RPAREN) : $2 (+ expr (prec: unary+)) : `(+ ,$2) (- expr (prec: unary-)) : `(- ,$2) (BITNOT expr) : `(lognot ,$2) (LOGNOT expr) : (make-bool `(zero? ,$2)) (expr * expr) : `(* ,$1 ,$3) (expr / expr) : `(quotient ,$1 ,$3) (expr % expr) : `(modulo ,$1 ,$3) (expr + expr) : `(+ ,$1 ,$3) (expr - expr) : `(- ,$1 ,$3) (expr << expr) : `(ash ,$1 ,$3) (expr >> expr) : `(ash ,$1 (- ,$3)) (expr < expr) : (make-bool `(< ,$1 ,$3)) (expr <= expr) : (make-bool `(<= ,$1 ,$3)) (expr > expr) : (make-bool `(> ,$1 ,$3)) (expr >= expr) : (make-bool `(>= ,$1 ,$3)) (expr == expr) : (make-bool `(= ,$1 ,$3)) (expr != expr) : (make-bool `(not (= ,$1 ,$3))) (expr BITAND expr) : `(logand ,$1 ,$3) (expr BITXOR expr) : `(logxor ,$1 ,$3) (expr BITIOR expr) : `(logior ,$1 ,$3) (expr LOGAND expr) : (make-bool `(and ,(nonzero? $1) ,(nonzero? $3))) (expr LOGIOR expr) : (make-bool `(or ,(nonzero? $1) ,(nonzero? $3))) (expr ? expr : expr) : `(if ,(nonzero? $1) ,$3 ,$5) (NAME = expr) : (make-assign $1 $3) (NAME *= expr) : (make-assign $1 $3 '*) (NAME /= expr) : (make-assign $1 $3 'quotient) (NAME %= expr) : (make-assign $1 $3 'modulo) (NAME += expr) : (make-assign $1 $3 '+) (NAME -= expr) : (make-assign $1 $3 '-) (NAME <<= expr) : (make-assign $1 $3 'ash) (NAME >>= expr) : (make-assign $1 $3 (lambda (x y) `(ash ,x (- ,y)))) (NAME BITAND-ASSIGN expr) : (make-assign $1 $3 'logand) (NAME BITXOR-ASSIGN expr) : (make-assign $1 $3 'logxor) (NAME BITIOR-ASSIGN expr) : (make-assign $1 $3 'logior)))) (define (read-arithmetic str) "Read @var{str} as an arithmetic expansion expression and return an equivalent Scheme expression." (let ((lexer (make-lexer str)) (parser (make-parser))) (parser lexer error)))