packages feed

cpsa-2.2.2: src/cpsaextras/SExpr.scala

/*
 * S-expressions
 *
'* Copyright (c) 2010 The MITRE Corporation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the BSD License as published by the University
 * of California.
 */

package cpsaextras

import java.io._

/**
 * Annotated S-expressions.  Each S-expression can be annotated with
 * something of type A.
 */
sealed abstract class SExpr[A] {
  /** The annotation. */
  val a: A
}
/** Case of a symbol. */
case class S[A](a: A, s: String) extends SExpr[A] {
  override def toString(): String = {
    s.toString()
  }
}
/** Case of a string. */
case class Q[A](a: A, q: String) extends SExpr[A] {
  override def toString(): String = {
    "\"" + q.toString() + "\""
  }
}
/** Case of a number. */
case class N[A](a: A, n: Int) extends SExpr[A] {
  override def toString(): String = {
    n.toString()
  }
}
/** Case of a list of S-expressions. */
case class L[A](a: A, l: List[SExpr[A]]) extends SExpr[A] {
  override def toString(): String = {
    l.toString()
  }
}

/** The file position annotation. */
final case class Pos(file: String, line: Int, column: Int) {
  override def toString (): String = {
    file + ":" + line + ":" + column + ": "
  }
}

/** An S-expression reader.
 *
 * @param file name of the input file
 * @param in stream associated with the input file
 */
class SExprReader(file: String, in: Reader) {
  private var line = 1			// Stream line number
  private var column = 1		// Stream column

  // A scanner token
  private sealed abstract class Token
  // A token for a non-list S-expression
  private case class Atom(a: SExpr[Pos]) extends Token
  // A token for the left parenthesis
  private case class LParen(p: Pos) extends Token
  // A token for the right parenthesis
  private case class RParen(p: Pos) extends Token
  // A token for end-of-file
  private case object Eof extends Token

  /**
   * Reads an S-expression annotated with file positions.
   * @return an S-expression or null on end-of-file
   */
  def read(): SExpr[Pos] = {
    scan() match {
      case Atom(a) => a
      case LParen(p) => list(p, Nil)
      case RParen(p) =>
	in.close()
	throw new IOException(p + "Close of unopened list")
      case Eof =>
	in.close()
	null
    }
  }

  // Parse a non top-level list
  private def list(pos: Pos, acc: List[SExpr[Pos]]): SExpr[Pos] = {
    scan() match {
      case Atom(a) => list(pos, a :: acc)
      case LParen(p) => list(pos, list(p, Nil) :: acc)
      case RParen(_) => L(pos, acc.reverse)
      case Eof =>
	in.close()
	throw new IOException(pos + "Unexpected end of input in list")
    }
  }

  // Scanner

  // Consume white space and then dispatch on the first character
  private def scan(): Token = {
    val ch = get()
    if (Character.isWhitespace(ch))
      scan()
    else ch match {
      case -1 => Eof
      case '(' => LParen(Pos(file, line, column))
      case ')' => RParen(Pos(file, line, column))
      case ';' => comment()
      case _ => atom(ch)
    }
  }

  // Consume a comment and then resume scanning
  private def comment(): Token = {
    get() match {
      case -1 => Eof
      case '\n' => scan()
      case _ => comment()
    }
  }

  // Scan an atom
  private def atom(ch: Int): Token = {
    val p = Pos(file, line, column)	// Position of atom
    ch match {
      case '"' => string(p, new StringBuilder())
      case '+' => numOrSym(p, singleton(ch))
      case '-' => numOrSym(p, singleton(ch))
      case _ =>
	if (Character.isDigit(ch))
	  number(p, singleton(ch))
	else if (isSym(ch))
	  symbol(p, singleton(ch))
	else {
	  in.close()
	  throw new IOException(p + "Bad char in atom")
	}
    }
  }

  // Create a string buffer with one character
  private def singleton(ch: Int): StringBuilder = {
    val sb = new StringBuilder()
    sb.append(ch.toChar)
    sb
  }

  // Scan a string atom
  private def string(p: Pos, sb: StringBuilder): Token = {
    val ch = get()
    if (ch == -1) {
      in.close()
      throw new IOException(p + "End of input in string")
    }
    else if (ch == '"')
      Atom(Q(p, sb.toString().intern()))
    else if (isStr(ch)) {
      sb.append(ch.toChar)
      string(p, sb)
    }
    else {
      in.close()
      throw new IOException(p + "Bad char for string")
    }
  }

  // Scan a number atom
  private def number(p: Pos, sb: StringBuilder): Token = {
    val ch = peek()
    if (ch == -1)
      Atom(N(p, Integer.parseInt(sb.toString())))
    else if (Character.isDigit(ch)) {
      get()
      sb.append(ch.toChar)
      number(p, sb)
    }
    else if (isSym(ch)) {
      in.close()
      throw new IOException(p + "Bad char after number")
    }
    else
      Atom(N(p, Integer.parseInt(sb.toString())))
  }

  // See if an atom is an number or a symbol
  private def numOrSym(p: Pos, sb: StringBuilder): Token = {
    val ch = peek()
    if (ch == -1)
      symbol(p, sb)
    else if (Character.isDigit(ch))
      number(p, sb)
    else
      symbol(p, sb)
  }

  // Scan a symbol
  private def symbol(p: Pos, sb: StringBuilder): Token = {
    val ch = peek()
    if (ch == -1)
      Atom(S(p, sb.toString().intern()))
    else if (isSym(ch)) {
      get()
      sb.append(ch.toChar)
      symbol(p, sb)
    }
    else
      Atom(S(p, sb.toString().intern()))
  }

  // Is character a constituent of a symbol?
  private def isSym(ch: Int) = {
    ch match {
      case '+' => true
      case '-' => true
      case '*' => true
      case '/' => true
      case '<' => true
      case '=' => true
      case '>' => true
      case '!' => true
      case '?' => true
      case ':' => true
      case '$' => true
      case '%' => true
      case '_' => true
      case '&' => true
      case '~' => true
      case '^' => true
      case _ => Character.isLetterOrDigit(ch)
    }
  }

  // Is character a constituent of a string?
  private def isStr(ch: Int) = {
    ch match {
      case '"' => false
      case '\\' => false
      case _ => !Character.isISOControl(ch)
    }
  }

  // Character reader with look ahead

  private var peeked = false		// Is look ahead active?
  private var seen = 0			// peeked character

  // Get next character and set the current file position.
  private def get(): Int = {
    val next =
      if (peeked) {
	peeked = false
	seen
      }
      else
	in.read()
    if (next == '\n') {
      line += 1
      column = 1
    }
    else
      column += 1
    next
  }

  // Read ahead if need be
  private def peek(): Int = {
    if (peeked)
      seen
    else {
      peeked = true
      seen = in.read()
      seen
    }
  }
}