Compare commits

...

2 Commits

Author SHA1 Message Date
0e065e0d7c +if/else statement
+// comments
2025-05-19 12:42:46 +04:00
60db9f3b95 string concatenation 2025-05-19 11:26:00 +04:00
7 changed files with 334 additions and 16 deletions

159
docs/tutorial.md Normal file
View File

@ -0,0 +1,159 @@
# Ling tutorial
Ling is a very simple language, where we take only most important and popular features from
other scripts and languages. In particular, we adopt _principle of minimal confusion_[^1].
In other word, the code usually works as expected when you see it. So, nothing unusual.
# Expressions and blocks.
Everything is an expression in Ling. Even an empty block:
{
// empty block
}
>>> void
Block returns it last expression as "return value":
{
2 + 2
3 + 3
}
>>> 6
Same is without block:
3 + 3
>>> 6
If you don't want block to return anything, use `void`:
{
3 + 4
void
}
>>> void
Every construction is an expression that returns something (or `void`):
val limited = if( x > 100 ) 100 else x
You can use blocks in if statement, as expected:
val limited = if( x > 100 ) {
100 + x * 0.1
}
else
x
So the principles are:
- everything is an expression returning its last calculated value or `void`
- expression could be a `{ block }`
## Expression details
It is rather simple, like everywhere else:
sin(x * π/4) / 2.0
See [math](math.md) for more on it.
# Defining functions
fun check(amount) {
if( amount > 100 )
"anough"
else
"more"
}
You can use both `fn` and `fun`. Note that function declaration _is an expression returning callable_.
There are default parameters in Ling:
fn check(amount, prefix = "answer: ") {
prefix + if( amount > 100 )
"anough"
else
"more"
}
## Closures
Each __block has an isolated context that can be accessed from closures__. For example:
var counter = 1
// this is ok: coumter is incremented
def increment(amount=1) {
// use counter from a closure:
counter = counter + amount
}
val taskAlias = def someTask() {
// this obscures global outer var with a local one
var counter = 0
// ...
counter = 1
// ...
counter
}
As was told, `def` statement return callable for the function, it could be used as a parameter, or elsewhere
to call it:
// call the callable stored in the var
taskAlias()
// or directly:
someTask()
If you need to create _unnamed_ function, use alternative syntax (TBD, like { -> } ?)
# Integral data types
| type | description | literal samples |
|--------|---------------------------------|---------------------|
| Int | 64 bit signed | `1` `-22` `0x1FF` |
| Real | 64 bit double | `1.0`, `2e-11` |
| Bool | boolean | `true` `false` |
| String | unicode string, no limits | "hello" (see below) |
| Void | no value could exist, singleton | void |
| Null | missing value, singleton | null |
| Fn | callable type | |
## String details
### String operations
Concatenation is a `+`: `"hello " + name` works as expected. No confusion.
### Literals
String literal could be multiline:
"
Hello,
World!
"
>>> "Hello
World"
In that case compiler removes left margin and first/last empty lines. Note that it won't remove margin:
"Hello,
World
"
>>> "Hello,
World
"
because the first line has no margin in the literal.
# Comments
// single line comment
var result = null // here we will store the result

View File

@ -1,5 +1,6 @@
import com.vanniktech.maven.publish.SonatypeHost
import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
@ -29,6 +30,11 @@ kotlin {
browser()
nodejs()
}
@OptIn(ExperimentalWasmDsl::class)
wasmJs() {
browser()
nodejs()
}
sourceSets {
all {

View File

@ -88,8 +88,15 @@ class Compiler {
}
}
Token.Type.SINLGE_LINE_COMMENT, Token.Type.MULTILINE_COMMENT -> continue
Token.Type.SEMICOLON -> continue
Token.Type.LBRACE -> {
tokens.previous()
parseBlock(tokens)
}
Token.Type.RBRACE -> {
tokens.previous()
return null
@ -148,6 +155,8 @@ class Compiler {
}
}
Token.Type.STRING -> statement(t.pos, true) { ObjString(t.value) }
Token.Type.LPAREN -> {
// ( subexpr )
parseExpression(tokens)?.also {
@ -274,9 +283,49 @@ class Compiler {
"val" -> parseVarDeclaration(id.value, false, tokens)
"var" -> parseVarDeclaration(id.value, true, tokens)
"fn", "fun" -> parseFunctionDeclaration(tokens)
"if" -> parseIfStatement(tokens)
else -> null
}
private fun parseIfStatement(tokens: ListIterator<Token>): Statement {
var t = tokens.next()
val start = t.pos
if( t.type != Token.Type.LPAREN)
throw ScriptError(t.pos, "Bad if statement: expected '('")
val condition = parseExpression(tokens)
?: throw ScriptError(t.pos, "Bad if statement: expected expression")
t = tokens.next()
if( t.type != Token.Type.RPAREN)
throw ScriptError(t.pos, "Bad if statement: expected ')' after condition expression")
val ifBody = parseStatement(tokens) ?: throw ScriptError(t.pos, "Bad if statement: expected statement")
// could be else block:
val t2 = tokens.next()
// we generate different statements: optimization
return if( t2.type == Token.Type.ID && t2.value == "else") {
val elseBody = parseStatement(tokens) ?: throw ScriptError(t.pos, "Bad else statement: expected statement")
return statement(start) {
if (condition.execute(it).toBool())
ifBody.execute(it)
else
elseBody.execute(it)
}
}
else {
tokens.previous()
statement(start) {
if (condition.execute(it).toBool())
ifBody.execute(it)
else
ObjVoid
}
}
}
data class FnParamDef(
val name: String,
val pos: Pos,
@ -315,8 +364,6 @@ class Compiler {
params.add(FnParamDef(t.value, t.pos, defaultValue))
} while (true)
println("arglist: $params")
// Here we should be at open body
val fnStatements = parseBlock(tokens)
@ -349,7 +396,11 @@ class Compiler {
val t = tokens.next()
if (t.type != Token.Type.LBRACE)
throw ScriptError(t.pos, "Expected block body start: {")
return parseScript(t.pos, tokens).also {
val block = parseScript(t.pos, tokens)
return statement(t.pos) {
// block run on inner context:
block.execute(it.copy())
}.also {
val t1 = tokens.next()
if (t1.type != Token.Type.RBRACE)
throw ScriptError(t1.pos, "unbalanced braces: expected block body end: }")
@ -365,7 +416,7 @@ class Compiler {
var setNull = false
if (eqToken.type != Token.Type.ASSIGN) {
if (!mutable)
throw ScriptError(eqToken.pos, "Expected initializator: '=' after '$kind ${name}'")
throw ScriptError(eqToken.pos, "Expected initializer: '=' after '$kind ${name}'")
else {
tokens.previous()
setNull = true

View File

@ -52,7 +52,14 @@ private class Parser(fromPos: Pos) {
'+' -> Token("+", from, Token.Type.PLUS)
'-' -> Token("-", from, Token.Type.MINUS)
'*' -> Token("*", from, Token.Type.STAR)
'/' -> Token("/", from, Token.Type.SLASH)
'/' -> {
if( currentChar == '/') {
advance()
Token(loadToEnd().trim(), from, Token.Type.SINLGE_LINE_COMMENT)
}
else
Token("/", from, Token.Type.SLASH)
}
'%' -> Token("%", from, Token.Type.PERCENT)
'.' -> Token(".", from, Token.Type.DOT)
'<' -> {
@ -206,6 +213,31 @@ private class Parser(fromPos: Pos) {
return result.toString()
}
@Suppress("unused")
private fun loadUntil(endChars: Set<Char>): String {
return if (pos.end) ""
else {
val result = StringBuilder()
while (!pos.end) {
val ch = pos.currentChar
if (ch in endChars) break
result.append(ch)
pos.advance()
}
result.toString()
}
}
private fun loadToEnd(): String {
val result = StringBuilder()
val l = pos.line
do {
result.append(pos.currentChar)
advance()
} while (pos.line == l)
return result.toString()
}
/**
* next non-whitespace char (newline are skipped too) or null if EOF
*/

View File

@ -8,6 +8,7 @@ data class Token(val value: String, val pos: Pos, val type: Type) {
PLUS, MINUS, STAR, SLASH, ASSIGN,
EQ, NEQ, LT, LTE, GT, GTE,
AND, BITAND, OR, BITOR, NOT, DOT, ARROW, QUESTION, COLONCOLON, PERCENT,
SINLGE_LINE_COMMENT, MULTILINE_COMMENT,
EOF,
}

View File

@ -75,7 +75,7 @@ class PlusStatement(
val l = left.execute(context)
if (l is ObjString)
return ObjString(l.toString() + right.execute(context).toString())
return ObjString(l.toString() + right.execute(context).asStr)
if (l !is Numeric)
raise("left operand is not number: $l")

View File

@ -8,7 +8,7 @@ import kotlin.test.*
class ScriptTest {
@Test
fun parseNumbers() {
fun parseNumbersTest() {
fun check(expected: String, type: Token.Type, row: Int, col: Int, src: String, offset: Int = 0) {
val source = src.toSource()
assertEquals(
@ -48,7 +48,7 @@ class ScriptTest {
}
@Test
fun parse0() {
fun parse0Test() {
val src = """
println("Hello")
println( "world" )
@ -68,7 +68,7 @@ class ScriptTest {
}
@Test
fun parse1() {
fun parse1Test() {
val src = "2 + 7".toSource()
val p = parseLing(src).listIterator()
@ -79,7 +79,7 @@ class ScriptTest {
}
@Test
fun compileNumbers() = runTest {
fun compileNumbersTest() = runTest {
assertEquals(ObjInt(17), eval("17"))
assertEquals(ObjInt(17), eval("+17"))
assertEquals(ObjInt(-17), eval("-17"))
@ -95,7 +95,7 @@ class ScriptTest {
}
@Test
fun compileBuiltinCalls() = runTest {
fun compileBuiltinCallsTest() = runTest {
// println(eval("π"))
val pi = eval("Math.PI")
assertIs<ObjReal>(pi)
@ -107,7 +107,7 @@ class ScriptTest {
}
@Test
fun varsAndConsts() = runTest {
fun varsAndConstsTest() = runTest {
val context = Context()
assertEquals(ObjVoid,context.eval("""
val a = 17
@ -171,7 +171,7 @@ class ScriptTest {
}
@Test
fun testArithmeticOperators() = runTest {
fun arithmeticOperatorsTest() = runTest {
assertEquals(2, eval("5/2").toInt())
assertEquals(2.5, eval("5.0/2").toDouble())
assertEquals(2.5, eval("5/2.0").toDouble())
@ -190,7 +190,7 @@ class ScriptTest {
}
@Test
fun testArithmeticParenthesis() = runTest {
fun arithmeticParenthesisTest() = runTest {
assertEquals(17, eval("2 + 3 * 5").toInt())
assertEquals(17, eval("2 + (3 * 5)").toInt())
assertEquals(25, eval("(2 + 3) * 5").toInt())
@ -198,7 +198,13 @@ class ScriptTest {
}
@Test
fun testEqNeq() = runTest {
fun stringOpTest() = runTest {
assertEquals("foobar", eval(""" "foo" + "bar" """).toString())
assertEquals("foo17", eval(""" "foo" + 17 """).toString())
}
@Test
fun eqNeqTest() = runTest {
assertEquals(ObjBool(true), eval("val x = 2; x == 2"))
assertEquals(ObjBool(false), eval("val x = 3; x == 2"))
assertEquals(ObjBool(true), eval("val x = 3; x != 2"))
@ -214,7 +220,7 @@ class ScriptTest {
}
@Test
fun testGtLt() = runTest {
fun gtLtTest() = runTest {
assertTrue { eval("3 > 2").toBool() }
assertFalse { eval("3 > 3").toBool() }
assertTrue { eval("3 >= 2").toBool() }
@ -228,5 +234,68 @@ class ScriptTest {
assertFalse { eval("4 <= 3").toBool()}
}
@Test
fun ifTest() = runTest {
// if - single line
var context = Context()
context.eval("""
fn test1(n) {
var result = "more"
if( n >= 10 )
result = "enough"
result
}
""".trimIndent())
assertEquals("enough", context.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString())
// if - multiline (block)
context = Context()
context.eval("""
fn test1(n) {
var prefix = "answer: "
var result = "more"
if( n >= 10 ) {
var prefix = "bad:" // local prefix
prefix = "too bad:"
result = "enough"
}
prefix + result
}
""".trimIndent())
assertEquals("answer: enough", context.eval("test1(11)").toString())
assertEquals("answer: more", context.eval("test1(1)").toString())
// else single line1
context = Context()
context.eval("""
fn test1(n) {
if( n >= 10 )
"enough"
else
"more"
}
""".trimIndent())
assertEquals("enough", context.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString())
// if/else with blocks
context = Context()
context.eval("""
fn test1(n) {
if( n > 20 ) {
"too much"
} else if( n >= 10 ) {
"enough"
}
else {
"more"
}
}
""".trimIndent())
assertEquals("enough", context.eval("test1(11)").toString())
assertEquals("more", context.eval("test1(1)").toString())
assertEquals("too much", context.eval("test1(100)").toString())
}
}