Long road starts here
This commit is contained in:
commit
b220299fc5
33 files changed
+2129
No files matched your search
@@ -0,0 +1,12 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
data class Arguments(val list: List<Statement> ) {
|
||||
|
||||
val size by list::size
|
||||
|
||||
operator fun get(index: Int): Statement = list[index]
|
||||
|
||||
companion object {
|
||||
val EMPTY = Arguments(emptyList())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,264 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
import kotlin.math.pow
|
||||
|
||||
sealed class ObjType(name: String, val defaultValue: Obj? = null) {
|
||||
|
||||
class Str : ObjType("string", ObjString(""))
|
||||
class Int : ObjType("real", ObjReal(0.0))
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Descriptor for whatever object could be used as argument, return value,
|
||||
* field, etc.
|
||||
*/
|
||||
data class ObjDescriptor(
|
||||
val type: ObjType,
|
||||
val mutable: Boolean,
|
||||
)
|
||||
|
||||
data class MethodDescriptor(
|
||||
val args: Array<ObjDescriptor>,
|
||||
val result: ObjDescriptor
|
||||
)
|
||||
|
||||
/*
|
||||
Meta context contains map of symbols.
|
||||
|
||||
Each symbol can be:
|
||||
|
||||
- a var
|
||||
- a const
|
||||
- a function
|
||||
- a type alias
|
||||
- a class definition
|
||||
|
||||
Each have in common only its name.
|
||||
|
||||
Var has: type, value. Const is same as var but value is fixed. Function has args and return value,
|
||||
type alias has target type name. So we have to have something that denotes a _type_
|
||||
|
||||
*/
|
||||
|
||||
|
||||
//data class MetaContext(val symbols: MutableMap<String, > = mutableMapOf()) {
|
||||
//
|
||||
//}
|
||||
|
||||
|
||||
class Compiler {
|
||||
|
||||
fun compile(source: Source): Script {
|
||||
val tokens = parseLing(source).listIterator()
|
||||
val start = source.startPos
|
||||
// at this level: "global" context: just script to execute or new function
|
||||
// declaration forming closures
|
||||
val statements = mutableListOf<Statement>()
|
||||
while (parseStatement(tokens)?.also {
|
||||
statements += it
|
||||
} != null) {/**/
|
||||
}
|
||||
return Script(start, statements)
|
||||
}
|
||||
|
||||
private fun parseStatement(tokens: ListIterator<Token>): Statement? {
|
||||
val t = tokens.next()
|
||||
return when (t.type) {
|
||||
Token.Type.ID -> {
|
||||
// could be keyword, assignment or just the expression
|
||||
val next = tokens.next()
|
||||
if (next.type == Token.Type.EQ) {
|
||||
// this _is_ assignment statement
|
||||
AssignStatement(
|
||||
t.pos, t.value,
|
||||
parseExpression(tokens) ?: throw ScriptError(
|
||||
t.pos,
|
||||
"Expecting expression for assignment operator"
|
||||
)
|
||||
)
|
||||
}
|
||||
// not assignment, maybe keyword statement:
|
||||
// get back the token which is not '=':
|
||||
tokens.previous()
|
||||
// try keyword statement
|
||||
parseKeywordStatement(t, tokens)
|
||||
?: run {
|
||||
parseExpression(tokens)
|
||||
}
|
||||
}
|
||||
|
||||
Token.Type.EOF -> null
|
||||
|
||||
else -> {
|
||||
// could be expression
|
||||
tokens.previous()
|
||||
parseExpression(tokens)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseExpression(tokens: ListIterator<Token>, level: Int = 0): Statement? {
|
||||
if (level == lastLevel)
|
||||
return parseTerm(tokens)
|
||||
var lvalue = parseExpression(tokens, level + 1)
|
||||
if (lvalue == null) return null
|
||||
|
||||
while (true) {
|
||||
|
||||
val opToken = tokens.next()
|
||||
val op = byLevel[level][opToken.value]
|
||||
if (op == null) {
|
||||
tokens.previous()
|
||||
break
|
||||
}
|
||||
|
||||
val rvalue = parseExpression(tokens, level + 1)
|
||||
?: throw ScriptError(opToken.pos, "Expecting expression")
|
||||
|
||||
lvalue = op.generate(opToken.pos, lvalue!!, rvalue)
|
||||
}
|
||||
return lvalue
|
||||
}
|
||||
|
||||
fun parseTerm(tokens: ListIterator<Token>): Statement? {
|
||||
// call op
|
||||
// index op
|
||||
// unary op
|
||||
// parenthesis
|
||||
// number or string
|
||||
val t = tokens.next()
|
||||
// todoL var?
|
||||
return when (t.type) {
|
||||
// "+" -> statement { parseNumber(true,tokens) }??????
|
||||
// "-" -> statement { parseNumber(false,tokens) }
|
||||
// "~" -> statement(t.pos) { ObjInt( parseLong(tokens)) }
|
||||
|
||||
Token.Type.PLUS -> {
|
||||
val n = parseNumber(true, tokens)
|
||||
statement(t.pos) { n }
|
||||
}
|
||||
|
||||
Token.Type.MINUS -> {
|
||||
val n = parseNumber(false, tokens)
|
||||
statement(t.pos) { n }
|
||||
}
|
||||
|
||||
Token.Type.INT, Token.Type.REAL, Token.Type.HEX -> {
|
||||
tokens.previous()
|
||||
val n = parseNumber(true, tokens)
|
||||
statement(t.pos) { n }
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun parseLong(tokens: ListIterator<Token>): Long =
|
||||
// todo: hex notation?
|
||||
getLong(tokens)
|
||||
|
||||
fun getLong(tokens: ListIterator<Token>): Long {
|
||||
val t = tokens.next()
|
||||
if (t.type != Token.Type.INT) throw ScriptError(t.pos, "expected number here")
|
||||
return t.value.toLong()
|
||||
}
|
||||
|
||||
fun parseNumber(isPlus: Boolean, tokens: ListIterator<Token>): Obj {
|
||||
val t = tokens.next()
|
||||
return when (t.type) {
|
||||
Token.Type.INT, Token.Type.HEX -> {
|
||||
val n = t.value.toLong(if (t.type == Token.Type.HEX) 16 else 10)
|
||||
if (isPlus) ObjInt(n) else ObjInt(-n)
|
||||
}
|
||||
|
||||
Token.Type.REAL -> {
|
||||
val d = t.value.toDouble()
|
||||
if (isPlus) ObjReal(d) else ObjReal(-d)
|
||||
}
|
||||
|
||||
else -> {
|
||||
throw ScriptError(t.pos, "expected number")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse keyword-starting statenment.
|
||||
* @return parsed statement or null if, for example. [id] is not among keywords
|
||||
*/
|
||||
private fun parseKeywordStatement(id: Token, tokens: ListIterator<Token>): Statement? {
|
||||
return null
|
||||
}
|
||||
|
||||
// fun parseStatement(parser: Parser): Statement? =
|
||||
// parser.withToken {
|
||||
// if (tokens.isEmpty()) null
|
||||
// else {
|
||||
// when (val token = tokens[0]) {
|
||||
// else -> {
|
||||
// rollback()
|
||||
// null
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
companion object {
|
||||
data class Operator(
|
||||
val name: String,
|
||||
val priority: Int, val arity: Int,
|
||||
val generate: (Pos, Statement, Statement) -> Statement
|
||||
)
|
||||
|
||||
val allOps = listOf(
|
||||
// Operator("||", 0, 2) { pos, a, b -> LogicalOrStatement(pos, a, b) })
|
||||
Operator("&&", 1, 2) { pos, a, b ->
|
||||
LogicalAndStatement(pos, a, b)
|
||||
},
|
||||
// bitwise or 2
|
||||
// bitwise and 3
|
||||
// equality/ne 4
|
||||
// relational <=,... 5
|
||||
// shuttle <=> 6
|
||||
// bitshhifts 7
|
||||
// + - : 7
|
||||
Operator("+", 7, 2) { pos, a, b ->
|
||||
PlusStatement(pos, a, b)
|
||||
},
|
||||
Operator("-", 7, 2) { pos, a, b ->
|
||||
MinusStatement(pos, a, b)
|
||||
},
|
||||
// * / %: 8
|
||||
)
|
||||
val lastLevel = 9
|
||||
val byLevel: List<Map<String, Operator>> = (0..<lastLevel).map { l ->
|
||||
allOps.filter { it.priority == l }
|
||||
.map { it.name to it }.toMap()
|
||||
}
|
||||
|
||||
fun compile(code: String): Script = Compiler().compile(Source("<eval>", code))
|
||||
}
|
||||
}
|
||||
|
||||
fun buildDoubleFromParts(
|
||||
integerPart: Long,
|
||||
decimalPart: Long,
|
||||
exponent: Int
|
||||
): Double {
|
||||
// Handle zero decimal case efficiently
|
||||
val numDecimalDigits = if (decimalPart == 0L) 0 else decimalPart.toString().length
|
||||
|
||||
// Calculate decimal multiplier (10^-digits)
|
||||
val decimalMultiplier = 10.0.pow(-numDecimalDigits)
|
||||
|
||||
// Combine integer and decimal parts
|
||||
val baseValue = integerPart.toDouble() + decimalPart.toDouble() * decimalMultiplier
|
||||
|
||||
// Apply exponent
|
||||
return baseValue * 10.0.pow(exponent)
|
||||
}
|
||||
|
||||
suspend fun eval(code: String) = Compiler.compile(code).execute()
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
import kotlin.math.PI
|
||||
|
||||
class Context(
|
||||
val callerPos: Pos,
|
||||
val parent: Context? = null,
|
||||
val args: Arguments = Arguments.EMPTY
|
||||
) {
|
||||
|
||||
data class Item(
|
||||
val name: String, var value: Obj?,
|
||||
val isMutable: Boolean = false
|
||||
)
|
||||
|
||||
private val objects = mutableMapOf<String, Item>()
|
||||
|
||||
operator fun get(name: String): Item? = objects[name] ?: parent?.get(name)
|
||||
|
||||
fun copy(from: Pos, args: Arguments = Arguments.EMPTY): Context = Context(from, this, args)
|
||||
|
||||
fun addFn(name: String, fn: suspend Context.() -> Obj) = objects.put(name, Item(name,
|
||||
object : Statement() {
|
||||
override val pos: Pos = Pos.builtIn
|
||||
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
return try {
|
||||
context.fn()
|
||||
} catch (e: Exception) {
|
||||
raise(e.message ?: "unexpected error")
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
val basicContext = Context(Pos.builtIn).apply {
|
||||
addFn("println") {
|
||||
require(args.size == 1)
|
||||
println(args[0].execute(this).asStr.value)
|
||||
Void
|
||||
}
|
||||
addFn("π") { ObjReal(PI) }
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.math.floor
|
||||
|
||||
@Serializable
|
||||
sealed class Obj {
|
||||
open val asStr: ObjString by lazy {
|
||||
if( this is ObjString) this else ObjString(this.toString())
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("void")
|
||||
object Void: Obj() {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is Void || other is Unit
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("string")
|
||||
data class ObjString(val value: String): Obj() {
|
||||
override fun toString(): String = value
|
||||
}
|
||||
|
||||
interface Numeric {
|
||||
val longValue: Long
|
||||
val doubleValue: Double
|
||||
val toObjInt: ObjInt
|
||||
val toObjReal: ObjReal
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("real")
|
||||
data class ObjReal(val value: Double): Obj(), Numeric {
|
||||
override val asStr by lazy { ObjString(value.toString()) }
|
||||
override val longValue: Long by lazy { floor(value).toLong() }
|
||||
override val doubleValue: Double by lazy { value }
|
||||
override val toObjInt: ObjInt by lazy { ObjInt(longValue) }
|
||||
override val toObjReal: ObjReal by lazy { ObjReal(value) }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("int")
|
||||
data class ObjInt(val value: Long): Obj(), Numeric {
|
||||
override val asStr by lazy { ObjString(value.toString()) }
|
||||
override val longValue: Long by lazy { value }
|
||||
override val doubleValue: Double by lazy { value.toDouble() }
|
||||
override val toObjInt: ObjInt by lazy { ObjInt(value) }
|
||||
override val toObjReal: ObjReal by lazy { ObjReal(doubleValue) }
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("bool")
|
||||
data class ObjBool(val value: Boolean): Obj() {
|
||||
override val asStr by lazy { ObjString(value.toString()) }
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
private val idFirstChars: Set<Char> = (
|
||||
('a'..'z') + ('A'..'Z') + '_' + ('а'..'я') + ('А'..'Я')
|
||||
).toSet()
|
||||
val idNextChars: Set<Char> = idFirstChars + ('0'..'9')
|
||||
val digits = ('0'..'9').toSet()
|
||||
val hexDigits = digits + ('a'..'f') + ('A'..'F')
|
||||
|
||||
fun parseLing(source: Source): List<Token> {
|
||||
val p = Parser(fromPos = source.startPos)
|
||||
val tokens = mutableListOf<Token>()
|
||||
do {
|
||||
val t = p.nextToken()
|
||||
tokens += t
|
||||
} while(t.type != Token.Type.EOF)
|
||||
return tokens
|
||||
}
|
||||
|
||||
private class Parser(fromPos: Pos) {
|
||||
|
||||
private val pos = MutablePos(fromPos)
|
||||
|
||||
/**
|
||||
* Immutable copy of current position
|
||||
*/
|
||||
private val currentPos: Pos get() = pos.toPos()
|
||||
|
||||
private fun raise(msg: String): Nothing = throw ScriptError(currentPos, msg)
|
||||
|
||||
fun nextToken(): Token {
|
||||
skipws()
|
||||
if (pos.end) return Token("", currentPos, Token.Type.EOF)
|
||||
val from = currentPos
|
||||
return when (val ch = pos.currentChar.also { advance() }) {
|
||||
'(' -> Token("(", from, Token.Type.LPAREN)
|
||||
')' -> Token(")", from, Token.Type.RPAREN)
|
||||
'{' -> Token("{", from, Token.Type.LBRACE)
|
||||
'}' -> Token("}", from, Token.Type.RBRACE)
|
||||
'[' -> Token("[", from, Token.Type.LBRACKET)
|
||||
']' -> Token("]", from, Token.Type.RBRACKET)
|
||||
',' -> Token(",", from, Token.Type.COMMA)
|
||||
';' -> Token(";", from, Token.Type.SEMICOLON)
|
||||
'=' -> Token("=", from, Token.Type.ASSIGN)
|
||||
'+' -> Token("+", from, Token.Type.PLUS)
|
||||
'-' -> Token("-", from, Token.Type.MINUS)
|
||||
'*' -> Token("*", from, Token.Type.STAR)
|
||||
'/' -> Token("/", from, Token.Type.SLASH)
|
||||
'.' -> Token(".", from, Token.Type.DOT)
|
||||
'<' -> Token("<", from, Token.Type.LT)
|
||||
'>' -> Token(">", from, Token.Type.GT)
|
||||
'!' -> Token("!", from, Token.Type.NOT)
|
||||
'"' -> loadStringToken()
|
||||
in digits -> {
|
||||
pos.back()
|
||||
decodeNumber(loadChars(digits), from)
|
||||
}
|
||||
in idFirstChars -> {
|
||||
// it includes first char, so from current position
|
||||
Token(ch + loadChars(idNextChars), from, Token.Type.ID)
|
||||
}
|
||||
else -> raise("can't parse token")
|
||||
}
|
||||
}
|
||||
|
||||
private fun decodeNumber(p1: String, start: Pos): Token =
|
||||
if( pos.end )
|
||||
Token(p1, start, Token.Type.INT)
|
||||
else if( currentChar == '.' ) {
|
||||
// could be decimal
|
||||
advance()
|
||||
if( currentChar in digits ) {
|
||||
// decimal part
|
||||
val p2 = loadChars(digits)
|
||||
// with exponent?
|
||||
if( currentChar == 'e' || currentChar == 'E') {
|
||||
advance()
|
||||
var negative = false
|
||||
if(currentChar == '+' )
|
||||
advance()
|
||||
else if(currentChar == '-') {
|
||||
negative = true
|
||||
advance()
|
||||
}
|
||||
var p3 = loadChars(digits)
|
||||
if( negative ) p3 = "-$p3"
|
||||
Token("$p1.${p2}e$p3", start, Token.Type.REAL)
|
||||
}
|
||||
else {
|
||||
// no exponent
|
||||
Token("$p1.$p2", start, Token.Type.REAL)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// not decimal
|
||||
// something like 10.times, method call on integer number
|
||||
pos.back()
|
||||
Token(p1, start, Token.Type.INT)
|
||||
}
|
||||
}
|
||||
else {
|
||||
// could be integer, also hex:
|
||||
if (currentChar == 'x' && p1 == "0") {
|
||||
advance()
|
||||
Token(loadChars(hexDigits), start, Token.Type.HEX).also {
|
||||
if( currentChar.isLetter() )
|
||||
raise("invalid hex literal")
|
||||
}
|
||||
} else {
|
||||
Token(p1, start, Token.Type.INT)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private val currentChar: Char get() = pos.currentChar
|
||||
|
||||
private fun loadStringToken(): Token {
|
||||
var start = currentPos
|
||||
|
||||
if (currentChar == '"') advance()
|
||||
else start = start.back()
|
||||
|
||||
val sb = StringBuilder()
|
||||
while (currentChar != '"') {
|
||||
if( pos.end ) raise("unterminated string")
|
||||
when (currentChar) {
|
||||
'\\' -> {
|
||||
advance() ?: raise("unterminated string")
|
||||
when (currentChar) {
|
||||
'n' -> sb.append('\n')
|
||||
'r' -> sb.append('\r')
|
||||
't' -> sb.append('\t')
|
||||
'"' -> sb.append('"')
|
||||
else -> sb.append('\\').append(currentChar)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
sb.append(currentChar)
|
||||
advance()
|
||||
}
|
||||
}
|
||||
}
|
||||
advance()
|
||||
return Token(sb.toString(), start, Token.Type.STRING)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load characters from the set until it reaches EOF or invalid character found.
|
||||
* stop at EOF on character not in [validChars].
|
||||
* @return the string of valid characters, could be empty
|
||||
*/
|
||||
private fun loadChars(validChars: Set<Char>): String {
|
||||
val result = StringBuilder()
|
||||
while (!pos.end) {
|
||||
val ch = pos.currentChar
|
||||
if (ch in validChars) {
|
||||
result.append(ch)
|
||||
advance()
|
||||
} else
|
||||
break
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* next non-whitespace char (newline are skipped too) or null if EOF
|
||||
*/
|
||||
private fun skipws(): Char? {
|
||||
while (!pos.end) {
|
||||
val ch = pos.currentChar
|
||||
if (ch.isWhitespace())
|
||||
advance()
|
||||
else
|
||||
return ch
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun advance() = pos.advance()
|
||||
|
||||
init {
|
||||
// advance()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
data class Pos(val source: Source, val line: Int, val column: Int) {
|
||||
override fun toString(): String {
|
||||
return "${source.fileName}:$line:$column"
|
||||
}
|
||||
|
||||
fun back(): Pos =
|
||||
if( column > 0) Pos(source, line, column-1)
|
||||
else if( line > 0) Pos(source, line-1, source.lines[line-1].length - 1)
|
||||
else throw IllegalStateException("can't go back from line 0, column 0")
|
||||
|
||||
val currentLine: String get() = source.lines[line]
|
||||
|
||||
val showSource: String by lazy {
|
||||
source.lines[line] + "\n" + "-".repeat(column - 1) + "^\n"
|
||||
}
|
||||
|
||||
companion object {
|
||||
val builtIn = Pos(Source.builtIn, 0, 0)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class MutablePos(private val from: Pos) {
|
||||
|
||||
val lines = from.source.lines
|
||||
var line = from.line
|
||||
private set
|
||||
var column = from.column
|
||||
private set
|
||||
|
||||
val end: Boolean get() =
|
||||
line == lines.size
|
||||
|
||||
fun reset(to: Pos) {
|
||||
line = to.line
|
||||
column = to.column
|
||||
}
|
||||
|
||||
fun toPos(): Pos = Pos(from.source, line, column)
|
||||
|
||||
fun advance(): Char? {
|
||||
if (end) return null
|
||||
val current = lines[line]
|
||||
return if (column+1 < current.length) {
|
||||
current[column++]
|
||||
} else {
|
||||
line++
|
||||
column = 0
|
||||
if(line >= lines.size) null
|
||||
else lines[line][column]
|
||||
}
|
||||
}
|
||||
|
||||
fun back() {
|
||||
if (column > 0) column--
|
||||
else if (line > 0)
|
||||
column = lines[--line].length - 1
|
||||
else throw IllegalStateException("can't go back from line 0, column 0")
|
||||
}
|
||||
val currentChar: Char
|
||||
get() =
|
||||
if (end) 0.toChar()
|
||||
else lines[line][column]
|
||||
|
||||
override fun toString() = "($line:$column)"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
class Script(
|
||||
override val pos: Pos,
|
||||
private val statements: List<Statement> = emptyList(),
|
||||
) : Statement() {
|
||||
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
// todo: run script
|
||||
var lastResult: Obj = Void
|
||||
for (s in statements) {
|
||||
lastResult = s.execute(context)
|
||||
}
|
||||
return lastResult
|
||||
}
|
||||
|
||||
suspend fun execute() = execute(defaultContext)
|
||||
|
||||
companion object {
|
||||
val defaultContext: Context = Context(Pos.builtIn)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
class ScriptError(val pos: Pos, val errorMessage: String) : Exception(
|
||||
"""
|
||||
$pos: Error: $errorMessage
|
||||
${pos.currentLine}
|
||||
${"-".repeat(pos.column)}^
|
||||
""".trimIndent()
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
class Source(val fileName: String, text: String) {
|
||||
|
||||
val lines = text.lines().map { it.trimEnd() }
|
||||
|
||||
companion object {
|
||||
val builtIn: Source by lazy { Source("built-in", "") }
|
||||
}
|
||||
|
||||
val startPos: Pos = Pos(this, 0, 0)
|
||||
|
||||
fun posAt(line: Int, column: Int): Pos = Pos(this, line, column)
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
data class Token(val value: String, val pos: Pos, val type: Type) {
|
||||
enum class Type {
|
||||
ID, INT, REAL, HEX, STRING, LPAREN, RPAREN, LBRACE, RBRACE, LBRACKET, RBRACKET, COMMA,
|
||||
SEMICOLON, COLON, EQ, PLUS, MINUS, STAR, SLASH, ASSIGN, EQEQ, NEQ, LT, LTEQ, GT,
|
||||
GTEQ, AND, OR, NOT, DOT, ARROW, QUESTION, COLONCOLON, EOF,
|
||||
}
|
||||
companion object {
|
||||
// fun eof(parser: Parser) = Token("", parser.currentPos, Type.EOF)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package net.sergeych.ling
|
||||
|
||||
fun String.toSource(name: String = "eval"): Source = Source(name, this)
|
||||
|
||||
|
||||
abstract class Statement : Obj() {
|
||||
abstract val pos: Pos
|
||||
abstract suspend fun execute(context: Context): Obj
|
||||
}
|
||||
|
||||
fun Statement.raise(text: String): Nothing {
|
||||
throw ScriptError(pos, text)
|
||||
}
|
||||
|
||||
fun Statement.require(cond: Boolean, message: () -> String) {
|
||||
if (!cond) raise(message())
|
||||
}
|
||||
|
||||
fun statement(pos: Pos, f: suspend (Context) -> Obj): Statement = object : Statement() {
|
||||
override val pos: Pos = pos
|
||||
override suspend fun execute(context: Context): Obj = f(context)
|
||||
}
|
||||
|
||||
class IfStatement(
|
||||
override val pos: Pos,
|
||||
val cond: Statement, val ifTrue: Statement, val ifFalse: Statement?
|
||||
) : Statement() {
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
val c = cond.execute(context)
|
||||
if (c !is ObjBool)
|
||||
raise("if: condition must me boolean, got: $c")
|
||||
|
||||
return if (c.value) ifTrue.execute(context) else ifFalse?.execute(context) ?: Void
|
||||
}
|
||||
}
|
||||
|
||||
class LogicalAndStatement(
|
||||
override val pos: Pos,
|
||||
val left: Statement, val right: Statement
|
||||
) : Statement() {
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
|
||||
val l = left.execute(context).let {
|
||||
(it as? ObjBool) ?: raise("logical and: left operand is not boolean: $it")
|
||||
}
|
||||
val r = right.execute(context).let {
|
||||
(it as? ObjBool) ?: raise("logical and: right operand is not boolean: $it")
|
||||
}
|
||||
return ObjBool(l.value && r.value)
|
||||
}
|
||||
}
|
||||
|
||||
class PlusStatement(
|
||||
override val pos: Pos,
|
||||
val left: Statement, val right: Statement
|
||||
) : Statement() {
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
// todo: implement also classes with 'plus' operator
|
||||
val l = left.execute(context)
|
||||
|
||||
if (l is ObjString)
|
||||
return ObjString(l.toString() + right.execute(context).toString())
|
||||
|
||||
if (l !is Numeric)
|
||||
raise("left operand is not number: $l")
|
||||
|
||||
val r = right.execute(context)
|
||||
if (r !is Numeric)
|
||||
raise("right operand is not boolean: $r")
|
||||
|
||||
return if (l is ObjInt && r is ObjInt)
|
||||
ObjInt(l.longValue + r.longValue)
|
||||
else
|
||||
ObjReal(l.doubleValue + r.doubleValue)
|
||||
}
|
||||
}
|
||||
|
||||
class MinusStatement(
|
||||
override val pos: Pos,
|
||||
val left: Statement, val right: Statement
|
||||
) : Statement() {
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
// todo: implement also classes with 'minus' operator
|
||||
val l = left.execute(context)
|
||||
if (l !is Numeric)
|
||||
raise("left operand is not number: $l")
|
||||
|
||||
val r = right.execute(context)
|
||||
if (r !is Numeric)
|
||||
raise("right operand is not number: $r")
|
||||
|
||||
return if (l is ObjInt && r is ObjInt)
|
||||
ObjInt(l.longValue - r.longValue)
|
||||
else
|
||||
ObjReal(l.doubleValue - r.doubleValue)
|
||||
}
|
||||
}
|
||||
|
||||
class AssignStatement(override val pos: Pos, val name: String, val value: Statement) : Statement() {
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
val variable = context[name] ?: raise("can't assign: variable does not exist: $name")
|
||||
variable.value = value.execute(context)
|
||||
return Void
|
||||
}
|
||||
}
|
||||
|
||||
class CallStatement(
|
||||
override val pos: Pos,
|
||||
val name: String,
|
||||
val args: Arguments = Arguments.EMPTY
|
||||
) : Statement() {
|
||||
override suspend fun execute(context: Context): Obj {
|
||||
val callee = context[name] ?: raise("Call: unknown name: $name")
|
||||
return (callee.value as? Statement)?.execute(context.copy(pos, args))
|
||||
?: raise("Call: not a callable object: $callee")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
package io.github.kotlin.fibonacci
|
||||
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import net.sergeych.ling.*
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class ScriptTest {
|
||||
|
||||
@Test
|
||||
fun level0() = runTest {
|
||||
val s = Script(
|
||||
Pos.builtIn,
|
||||
listOf(
|
||||
CallStatement(
|
||||
Pos.builtIn, "println",
|
||||
Arguments(listOf(CallStatement(Pos.builtIn, "π", Arguments.EMPTY)))
|
||||
)
|
||||
)
|
||||
)
|
||||
s.execute(basicContext)
|
||||
}
|
||||
|
||||
fun parseFirst(str: String): Token =
|
||||
parseLing(str.toSource()).firstOrNull()!!
|
||||
|
||||
@Test
|
||||
fun parseNumbers() {
|
||||
fun check(expected: String, type: Token.Type, row: Int, col: Int, src: String, offset: Int = 0) {
|
||||
val source = src.toSource()
|
||||
assertEquals(
|
||||
Token(expected, source.posAt(row, col), type),
|
||||
parseLing(source)[offset]
|
||||
)
|
||||
}
|
||||
check("1", Token.Type.INT, 0, 0, "1")
|
||||
check("7", Token.Type.INT, 0, 0, "7")
|
||||
check("17", Token.Type.INT, 0, 0, "17")
|
||||
check("17", Token.Type.INT, 0, 0, "17.")
|
||||
check(".", Token.Type.DOT, 0, 2, "17.", 1)
|
||||
|
||||
// decimals
|
||||
check("17.2", Token.Type.REAL, 0, 0, "17.2")
|
||||
check("17.2", Token.Type.REAL, 0, 0, "17.2")
|
||||
check("17.2", Token.Type.REAL, 0, 0, "17.2 ")
|
||||
check("17.2", Token.Type.REAL, 0, 1, " 17.2")
|
||||
check("17.2", Token.Type.REAL, 0, 2, " 17.2 ")
|
||||
check("17.2e0", Token.Type.REAL, 0, 0, "17.2e0")
|
||||
check("17.2e-22", Token.Type.REAL, 0, 0, "17.2e-22")
|
||||
check("17.2e22", Token.Type.REAL, 0, 0, "17.2e+22")
|
||||
check("17.2e22", Token.Type.REAL, 0, 0, "17.2E+22")
|
||||
check("17.2e22", Token.Type.REAL, 0, 0, "17.2E22")
|
||||
check("17.2e-22", Token.Type.REAL, 0, 0, "17.2E-22")
|
||||
|
||||
// hex
|
||||
check("1", Token.Type.HEX, 0, 0, "0x1")
|
||||
check("12", Token.Type.HEX, 0, 0, "0x12")
|
||||
check("12abcdef", Token.Type.HEX, 0, 0, "0x12abcdef.gh")
|
||||
check(".", Token.Type.DOT, 0, 10, "0x12abcdef.gh", 1)
|
||||
check("gh", Token.Type.ID, 0, 11, "0x12abcdef.gh", 2)
|
||||
|
||||
check("5", Token.Type.INT, 0, 0, "5 6")
|
||||
check("6", Token.Type.INT, 0, 2, "5 6", 1)
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parse0() {
|
||||
val src = """
|
||||
println("Hello")
|
||||
println( "world" )
|
||||
""".trimIndent().toSource()
|
||||
|
||||
val p = parseLing(src).listIterator()
|
||||
|
||||
assertEquals(Token("println", src.posAt(0, 0), Token.Type.ID), p.next())
|
||||
assertEquals(Token("(", src.posAt(0, 7), Token.Type.LPAREN), p.next())
|
||||
assertEquals(Token("Hello", src.posAt(0, 8), Token.Type.STRING), p.next())
|
||||
assertEquals(Token(")", src.posAt(0, 15), Token.Type.RPAREN), p.next())
|
||||
|
||||
assertEquals(Token("println", src.posAt(1, 0), Token.Type.ID), p.next())
|
||||
assertEquals(Token("(", src.posAt(1, 7), Token.Type.LPAREN), p.next())
|
||||
assertEquals(Token("world", src.posAt(1, 9), Token.Type.STRING), p.next())
|
||||
assertEquals(Token(")", src.posAt(1, 17), Token.Type.RPAREN), p.next())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun parse1() {
|
||||
val src = "2 + 7".toSource()
|
||||
|
||||
val p = parseLing(src).listIterator()
|
||||
|
||||
assertEquals(Token("2", src.posAt(0, 0), Token.Type.INT), p.next())
|
||||
assertEquals(Token("+", src.posAt(0, 2), Token.Type.PLUS), p.next())
|
||||
assertEquals(Token("7", src.posAt(0, 4), Token.Type.INT), p.next())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun compileNumbers() = runTest {
|
||||
assertEquals(ObjInt(17), eval("17"))
|
||||
assertEquals(ObjInt(17), eval("+17"))
|
||||
assertEquals(ObjInt(-17), eval("-17"))
|
||||
|
||||
|
||||
assertEquals(ObjInt(1970), eval("1900 + 70"))
|
||||
assertEquals(ObjInt(1970), eval("2000 - 30"))
|
||||
|
||||
// assertEquals(ObjReal(3.14), eval("3.14"))
|
||||
assertEquals(ObjReal(314.0), eval("3.14e2"))
|
||||
}
|
||||
|
||||
}
|
||||
Reference in new issue
Block a user