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
}
}
}