Long road starts here

This commit is contained in:
sergeych committed 2025-05-18 00:52:19 +04:00
commit b220299fc5
33 files changed
+2129

No files matched your search

+101
View File
@@ -0,0 +1,101 @@
import com.vanniktech.maven.publish.SonatypeHost
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.vanniktech.mavenPublish)
kotlin("plugin.serialization") version "2.1.20"
}
group = "net.sergeych"
version = "0.0.1-SNAPSHOT"
kotlin {
jvm()
androidTarget {
publishLibraryVariants("release")
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
}
}
// iosX64()
// iosArm64()
// iosSimulatorArm64()
linuxX64()
js(IR) {
browser()
nodejs()
}
sourceSets {
all {
languageSettings.optIn("kotlinx.coroutines.ExperimentalCoroutinesApi")
languageSettings.optIn("kotlin.ExperimentalUnsignedTypes")
languageSettings.optIn("kotlin.coroutines.DelicateCoroutinesApi")
}
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
//put your multiplatform dependencies here
implementation(libs.kotlinx.coroutines.core)
implementation(libs.mp.bintools)
}
}
val commonTest by getting {
dependencies {
implementation(libs.kotlin.test)
implementation(libs.kotlinx.coroutines.test)
}
}
}
}
android {
namespace = "org.jetbrains.kotlinx.multiplatform.library.template"
compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig {
minSdk = libs.versions.android.minSdk.get().toInt()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
}
mavenPublishing {
publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL)
signAllPublications()
coordinates(group.toString(), "library", version.toString())
pom {
name = "Ling language"
description = "Kotlin-bound scripting loanguage"
inceptionYear = "2025"
// url = "https://sergeych.net"
licenses {
license {
name = "XXX"
url = "YYY"
distribution = "ZZZ"
}
}
developers {
developer {
id = "XXX"
name = "YYY"
url = "ZZZ"
}
}
scm {
url = "XXX"
connection = "YYY"
developerConnection = "ZZZ"
}
}
}
@@ -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")
}
}
+112
View File
@@ -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"))
}
}