From 48cabbf4c086b2dd54fb25504f61e7494debb3d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89tienne=20Reuh=20Fildadut?= Date: Sun, 12 Dec 2021 17:07:50 +0100 Subject: [PATCH] Add while loop line and operator --- LANGUAGE.md | 34 ++++++-- interpreter/expression.lua | 17 ++++ interpreter/interpreter.lua | 75 +++++++++++------ notes.txt | 6 +- parser/common.lua | 2 +- parser/expression.lua | 4 +- parser/preparser.lua | 14 ++- stdlib/functions.lua | 20 ++--- test/tests/loop decorator.ans | 5 ++ test/tests/loop decorator.lua | 117 +++++++++++++++++++++++++ test/tests/while loop else.ans | 19 +++++ test/tests/while loop else.lua | 82 ++++++++++++++++++ test/tests/while loop.ans | 16 ++++ test/tests/while loop.lua | 150 +++++++++++++++++++++++++++++++++ 14 files changed, 500 insertions(+), 61 deletions(-) create mode 100644 test/tests/loop decorator.ans create mode 100644 test/tests/loop decorator.lua create mode 100644 test/tests/while loop else.ans create mode 100644 test/tests/while loop else.lua create mode 100644 test/tests/while loop.ans create mode 100644 test/tests/while loop.lua diff --git a/LANGUAGE.md b/LANGUAGE.md index 97842a7..4d98052 100644 --- a/LANGUAGE.md +++ b/LANGUAGE.md @@ -93,6 +93,24 @@ There's different types of lines, depending on their first character(s) (after i This is. ``` +* `~?`: while loop line. Works like `~` condition lines, but if the expression evaluates to true and after it ran its children, will reevaluate the expression again and repeat the previous logic until the expression eventually evaluates to false. + +``` +(Count to 10:) +:i = 1 +~? i < 10 + {i} + + ~ i += 1 + +(else-conditions can be used if the expression has never evalated to true in the loop:) +~ i := 5 +~? i < 2 + Never happens. +~~ + This is run. +``` + * `>`: write a choice into the [event buffer](#event-buffer). Followed by arbitrary text. Support [text interpolation](#text-interpolation); if a text event is created during the text interpolation, it is added to the choice text content instead of the global event buffer. Support [escape codes](#escape-codes). Empty choices are discarded. ``` @@ -105,7 +123,7 @@ $ f > {f} ``` -If an unescaped `~` or `#` appears in the line, the associated operator is applied to the line (see [operators](#operators)), using the previous text as the left argument and everything that follows as the right argument expression. +If an unescaped `~`, `~?` or `#` appears in the line, the associated operator is applied to the line (see [operators](#operators)), using the previous text as the left argument and everything that follows as the right argument expression. ``` (Conditionnaly executing a line) @@ -199,7 +217,7 @@ $ f(x::string) ~ f("hello") ``` -Every operator, except assignement operators, `|`, `&`, `,`, `~` and `#` can also be use as a function name in order to overload the operator: +Every operator, except assignement operators, `|`, `&`, `,`, `~?`, `~` and `#` can also be use as a function name in order to overload the operator: ``` (binary operator names: _op_) @@ -311,7 +329,7 @@ this is some text. And this is more text, in a different event. ``` -If an unescaped `~` or `#` appears in the line, the associated operator is applied to the line (see [operators](#operators)), using the previous text as the left argument and everything that follows as the right argument expression. +If an unescaped `~`, `~?` or `#` appears in the line, the associated operator is applied to the line (see [operators](#operators)), using the previous text as the left argument and everything that follows as the right argument expression. ``` (Conditionnaly executing a line) @@ -795,10 +813,10 @@ Please also be aware that when resuming from a checkpoint, Anselme will try to r From lowest to highest priority: ``` -_;_ +_;_ _; _:=_ _+=_ _-=_ _//=_ _/=_ _*=_ _%=_ _^=_ _,_ -_|_ _&_ _~_ _#_ +_|_ _&_ _~?_ _~_ _#_ _!=_ _==_ _>=_ _<=_ _<_ _>_ _+_ _-_ _*_ _//_ _/_ _%_ @@ -861,6 +879,10 @@ This only works on strings: `a | b`: or operator, lazy +`a ~ b`: evaluates b, if true evaluates a and returns it, otherwise returns nil (lazy). + +`a ~? b`: evaluates b, if true evaluates a then reevaluate b and loop this until b is false; returns a list containing all successive values that a returned. + ##### Functions and function references `fn(args)`: call the function, checkpoint or function reference with the given arguments. @@ -887,8 +909,6 @@ This only works on strings: `a :: b`: evaluate a and b, returns a new typed value with a as value and b as type. -`a ~ b`: evaluates b, if true evaluates a and returns it, otherwise returns nil (lazy). - `a # b`: evaluates b, then evaluates a whith b added to the active tags. Returns a. `a.b`: if a is a function reference, returns the variable named `b` in the referenced function namespace. When overloading this operator, if `b` is an identifier, the operator will interpret it as a string (and not returns the evaluated value of the variable eventually associated to the identifier). diff --git a/interpreter/expression.lua b/interpreter/expression.lua index 05ad756..a4e1e3e 100644 --- a/interpreter/expression.lua +++ b/interpreter/expression.lua @@ -122,6 +122,23 @@ local function eval(state, exp) type = "nil", value = nil } + -- while loop + elseif exp.type == "~?" then + local right, righte = eval(state, exp.right) + if not right then return right, righte end + local l = {} + while truthy(right) do + local left, lefte = eval(state, exp.left) + if not left then return left, lefte end + table.insert(l, left) + -- next iteration + right, righte = eval(state, exp.right) + if not right then return right, righte end + end + return { + type = "list", + value = l + } -- tag elseif exp.type == "#" then local right, righte = eval(state, exp.right) diff --git a/interpreter/interpreter.lua b/interpreter/interpreter.lua index 20c1d45..54d0621 100644 --- a/interpreter/interpreter.lua +++ b/interpreter/interpreter.lua @@ -30,40 +30,56 @@ run_line = function(state, line) if v then return v end end end + elseif line.type == "while" then + line.parent_block.last_condition_success = nil + local v, e = eval(state, line.expression) + if not v then return v, ("%s; at %s"):format(e, line.source) end + while truthy(v) do + line.parent_block.last_condition_success = true + v, e = run_block(state, line.child) + if e then return v, e end + if v then return v end + -- next iteration + v, e = eval(state, line.expression) + if not v then return v, ("%s; at %s"):format(e, line.source) end + end elseif line.type == "choice" then local v, e = events:make_space_for(state, "choice") if not v then return v, ("%s; in automatic event flush at %s"):format(e, line.source) end v, e = eval(state, line.text) if not v then return v, ("%s; at %s"):format(e, line.source) end + local l = v.type == "list" and v.value or { v } -- convert text events to choices - if v.type == "event buffer" then - local current_tags = tags:current(state) - local choice_block_state = { tags = current_tags, block = line.child } - local final_buffer = {} - for _, event in ipairs(v.value) do - if event.type == "text" then - -- create new choice block if needed - local last_choice_block = final_buffer[#final_buffer] - if not last_choice_block or last_choice_block.type ~= "choice" then - last_choice_block = { type = "choice", value = {} } - table.insert(final_buffer, last_choice_block) + for _, item in ipairs(l) do + if item.type == "event buffer" then + local current_tags = tags:current(state) + local choice_block_state = { tags = current_tags, block = line.child } + local final_buffer = {} + for _, event in ipairs(item.value) do + if event.type == "text" then + -- create new choice block if needed + local last_choice_block = final_buffer[#final_buffer] + if not last_choice_block or last_choice_block.type ~= "choice" then + last_choice_block = { type = "choice", value = {} } + table.insert(final_buffer, last_choice_block) + end + -- create new choice item in choice block if needed + local last_choice = last_choice_block.value[#last_choice_block.value] + if not last_choice then + last_choice = { _state = choice_block_state } + table.insert(last_choice_block.value, last_choice) + end + -- add text to last choice item + for _, txt in ipairs(event.value) do + table.insert(last_choice, txt) + end + else + table.insert(final_buffer, event) end - -- create new choice item in choice block if needed - local last_choice = last_choice_block.value[#last_choice_block.value] - if not last_choice then - last_choice = { _state = choice_block_state } - table.insert(last_choice_block.value, last_choice) - end - -- add text to last choice item - for _, txt in ipairs(event.value) do - table.insert(last_choice, txt) - end - else - table.insert(final_buffer, event) end + local iv, ie = events:write_buffer(state, final_buffer) + if not iv then return iv, ("%s; at %s"):format(ie, line.source) end end - v, e = events:write_buffer(state, final_buffer) - if not v then return v, ("%s; at %s"):format(e, line.source) end end elseif line.type == "tag" then local v, e = eval(state, line.expression) @@ -82,9 +98,12 @@ run_line = function(state, line) if not v then return v, ("%s; in automatic event flush at %s"):format(e, line.source) end v, e = eval(state, line.text) if not v then return v, ("%s; at %s"):format(e, line.source) end - if v.type == "event buffer" then - v, e = events:write_buffer(state, v.value) - if not v then return v, ("%s; at %s"):format(e, line.source) end + local l = v.type == "list" and v.value or { v } + for _, item in ipairs(l) do + if item.type == "event buffer" then + local iv, ie = events:write_buffer(state, item.value) + if not iv then return iv, ("%s; at %s"):format(ie, line.source) end + end end elseif line.type == "flush_events" then local v, e = events:flush(state) diff --git a/notes.txt b/notes.txt index e08548c..f7672c9 100644 --- a/notes.txt +++ b/notes.txt @@ -43,8 +43,6 @@ Reserved symbols that are still not used as a line type: `^+-= meh, this means we can capture choice events, and choice events contain their code block, and we don't want to store code blocks in the save file (as code can be updated/translated/whatever) @@ -74,7 +72,7 @@ TODO: fn/checkpoint/tag: maybe consider them a regular func call that takes chil # color:red a - translate to + translate to something like ~ tag.push(color:red) a @@ -89,7 +87,7 @@ TODO: make language simple enough to be able to reimplement it in, say, nim. Esp TODO: test reacheability of script paths -TODO: redisign the checkpoint system to work better when used with parallel scripts (if both change the same variable, will be overwritten) +TODO: redisign the checkpoint system to work better when used with parallel scripts (if both change the same variable, will be overwritten); not sure how to do that, would need some complicated conflict resolution code or something like CRDT... TODO: redisign a static type checking system If we want to go full gradual typing, it would help to: diff --git a/parser/common.lua b/parser/common.lua index 3bb6185..00c9c2a 100644 --- a/parser/common.lua +++ b/parser/common.lua @@ -31,7 +31,7 @@ common = { -- operators not included here and why: -- * assignment operators (:=, +=, -=, //=, /=, *=, %=, ^=): handled with its own syntax (function assignment) -- * list operator (,): is used when calling every functions, sounds like more trouble than it's worth - -- * |, & and ~ operators: are lazy and don't behave like regular functions + -- * |, &, ~? and ~ operators: are lazy and don't behave like regular functions -- * # operator: need to set tag state _before_ evaluating the left arg -- prefix unop diff --git a/parser/expression.lua b/parser/expression.lua index 7884566..6f90291 100644 --- a/parser/expression.lua +++ b/parser/expression.lua @@ -5,7 +5,7 @@ local binops_prio = { [1] = { ";" }, [2] = { ":=", "+=", "-=", "//=", "/=", "*=", "%=", "^=" }, [3] = { "," }, - [4] = { "|", "&", "~", "#" }, + [4] = { "|", "&", "~?", "~", "#" }, [5] = { "!=", "==", ">=", "<=", "<", ">" }, [6] = { "+", "-" }, [7] = { "*", "//", "/", "%" }, @@ -336,7 +336,7 @@ local function expression(s, state, namespace, current_priority, operating_on) left = operating_on, right = right }) - elseif op == "&" or op == "|" or op == "~" or op == "#" then + elseif op == "&" or op == "|" or op == "~?" or op == "~" or op == "#" then return expression(r, state, namespace, current_priority, { type = op, left = operating_on, diff --git a/parser/preparser.lua b/parser/preparser.lua index bd6e63e..6f94b08 100644 --- a/parser/preparser.lua +++ b/parser/preparser.lua @@ -29,11 +29,17 @@ local function parse_line(line, state, namespace) local r = { source = line.source } - -- else-condition & condition - if l:match("^~~?") then - r.type = l:match("^~~") and "else-condition" or "condition" + -- else-condition, condition & while + if l:match("^~[~%?]?") then + if l:match("^~~") then + r.type = "else-condition" + elseif l:match("^~%?") then + r.type = "while" + else + r.type = "condition" + end r.child = true - local expr = l:match("^~~?(.*)$") + local expr = l:match("^~[~%?]?(.*)$") if expr:match("[^%s]") then r.expression = expr else diff --git a/stdlib/functions.lua b/stdlib/functions.lua index adcb1ab..7468073 100644 --- a/stdlib/functions.lua +++ b/stdlib/functions.lua @@ -308,24 +308,14 @@ $ random(l...) ~ l(rand(1, l!len))! $ next(l...) - :f = l(len(l)) - $ find first not seen(j) - ~ l(j).👁️ == 0 - ~ f := l(j) - ~~ j < len(l) - ~ find first not seen(j+1) - ~ find first not seen(1) - ~ f! + :j = 0 + ~? j += 1; j < len(l) & l(j).👁️ != 0 + ~ l(j)! $ cycle(l...) :f = l(1) - $ find first smaller(j) - ~ l(j).👁️ < f.👁️ - ~ f := l(j) - ~~ j < len(l) - ~ find first smaller(j+1) - ~ len(l) > 1 - ~ find first smaller(2) + :j = 1 + ~? j += 1; j <= len(l) & !((f := l(j); 1) ~ l(j).👁️ < f.👁️) ~ f! ]] diff --git a/test/tests/loop decorator.ans b/test/tests/loop decorator.ans new file mode 100644 index 0000000..c932b8a --- /dev/null +++ b/test/tests/loop decorator.ans @@ -0,0 +1,5 @@ +:i = 0 + +{i}\n~? (i += 1; i <= 10) + +{i} diff --git a/test/tests/loop decorator.lua b/test/tests/loop decorator.lua new file mode 100644 index 0000000..6309837 --- /dev/null +++ b/test/tests/loop decorator.lua @@ -0,0 +1,117 @@ +local _={} +_[47]={} +_[46]={} +_[45]={} +_[44]={} +_[43]={} +_[42]={} +_[41]={} +_[40]={} +_[39]={} +_[38]={} +_[37]={} +_[36]={} +_[35]={} +_[34]={} +_[33]={} +_[32]={} +_[31]={} +_[30]={} +_[29]={} +_[28]={} +_[27]={} +_[26]={text="11",tags=_[47]} +_[25]={text="\n",tags=_[46]} +_[24]={text="10",tags=_[45]} +_[23]={text="\n",tags=_[44]} +_[22]={text="9",tags=_[43]} +_[21]={text="\n",tags=_[42]} +_[20]={text="8",tags=_[41]} +_[19]={text="\n",tags=_[40]} +_[18]={text="7",tags=_[39]} +_[17]={text="\n",tags=_[38]} +_[16]={text="6",tags=_[37]} +_[15]={text="\n",tags=_[36]} +_[14]={text="5",tags=_[35]} +_[13]={text="\n",tags=_[34]} +_[12]={text="4",tags=_[33]} +_[11]={text="\n",tags=_[32]} +_[10]={text="3",tags=_[31]} +_[9]={text="\n",tags=_[30]} +_[8]={text="2",tags=_[29]} +_[7]={text="\n",tags=_[28]} +_[6]={text="1",tags=_[27]} +_[5]={_[26]} +_[4]={_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13],_[14],_[15],_[16],_[17],_[18],_[19],_[20],_[21],_[22],_[23],_[24],_[25]} +_[3]={"return"} +_[2]={"text",_[5]} +_[1]={"text",_[4]} +return {_[1],_[2],_[3]} +--[[ +{ "text", { { + tags = {}, + text = "1" + }, { + tags = {}, + text = "\n" + }, { + tags = {}, + text = "2" + }, { + tags = {}, + text = "\n" + }, { + tags = {}, + text = "3" + }, { + tags = {}, + text = "\n" + }, { + tags = {}, + text = "4" + }, { + tags = {}, + text = "\n" + }, { + tags = {}, + text = "5" + }, { + tags = {}, + text = "\n" + }, { + tags = {}, + text = "6" + }, { + tags = {}, + text = "\n" + }, { + tags = {}, + text = "7" + }, { + tags = {}, + text = "\n" + }, { + tags = {}, + text = "8" + }, { + tags = {}, + text = "\n" + }, { + tags = {}, + text = "9" + }, { + tags = {}, + text = "\n" + }, { + tags = {}, + text = "10" + }, { + tags = {}, + text = "\n" + } } } +{ "text", { { + tags = {}, + text = "11" + } } } +{ "return" } +]]-- \ No newline at end of file diff --git a/test/tests/while loop else.ans b/test/tests/while loop else.ans new file mode 100644 index 0000000..5ef53f4 --- /dev/null +++ b/test/tests/while loop else.ans @@ -0,0 +1,19 @@ +:i = 1 + +Start with i={i}: + +~? i < 5 + {i} + + ~ i += 1 +~~ + Loop not ran. + +Start with i={i}: + +~? i < 5 + {i} + + ~ i += 1 +~~ + Loop not ran. \ No newline at end of file diff --git a/test/tests/while loop else.lua b/test/tests/while loop else.lua new file mode 100644 index 0000000..55f6864 --- /dev/null +++ b/test/tests/while loop else.lua @@ -0,0 +1,82 @@ +local _={} +_[37]={} +_[36]={} +_[35]={} +_[34]={} +_[33]={} +_[32]={} +_[31]={} +_[30]={} +_[29]={} +_[28]={} +_[27]={} +_[26]={text="Loop not ran.",tags=_[37]} +_[25]={text=":",tags=_[36]} +_[24]={text="5",tags=_[35]} +_[23]={text="Start with i=",tags=_[34]} +_[22]={text="4",tags=_[33]} +_[21]={text="3",tags=_[32]} +_[20]={text="2",tags=_[31]} +_[19]={text="1",tags=_[30]} +_[18]={text=":",tags=_[29]} +_[17]={text="1",tags=_[28]} +_[16]={text="Start with i=",tags=_[27]} +_[15]={_[26]} +_[14]={_[23],_[24],_[25]} +_[13]={_[22]} +_[12]={_[21]} +_[11]={_[20]} +_[10]={_[19]} +_[9]={_[16],_[17],_[18]} +_[8]={"return"} +_[7]={"text",_[15]} +_[6]={"text",_[14]} +_[5]={"text",_[13]} +_[4]={"text",_[12]} +_[3]={"text",_[11]} +_[2]={"text",_[10]} +_[1]={"text",_[9]} +return {_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8]} +--[[ +{ "text", { { + tags = {}, + text = "Start with i=" + }, { + tags = {}, + text = "1" + }, { + tags = {}, + text = ":" + } } } +{ "text", { { + tags = {}, + text = "1" + } } } +{ "text", { { + tags = {}, + text = "2" + } } } +{ "text", { { + tags = {}, + text = "3" + } } } +{ "text", { { + tags = {}, + text = "4" + } } } +{ "text", { { + tags = {}, + text = "Start with i=" + }, { + tags = {}, + text = "5" + }, { + tags = {}, + text = ":" + } } } +{ "text", { { + tags = {}, + text = "Loop not ran." + } } } +{ "return" } +]]-- \ No newline at end of file diff --git a/test/tests/while loop.ans b/test/tests/while loop.ans new file mode 100644 index 0000000..1932001 --- /dev/null +++ b/test/tests/while loop.ans @@ -0,0 +1,16 @@ +:i = 0 +~? i <= 10 + {i} + + ~ i += 1 + +return in loop: + +~ i := 0 +~? i <= 10 + {i} + + ~ i == 5 + @ + + ~ i += 1 diff --git a/test/tests/while loop.lua b/test/tests/while loop.lua new file mode 100644 index 0000000..07bfb82 --- /dev/null +++ b/test/tests/while loop.lua @@ -0,0 +1,150 @@ +local _={} +_[73]={} +_[72]={} +_[71]={} +_[70]={} +_[69]={} +_[68]={} +_[67]={} +_[66]={} +_[65]={} +_[64]={} +_[63]={} +_[62]={} +_[61]={} +_[60]={} +_[59]={} +_[58]={} +_[57]={} +_[56]={} +_[55]={text="5",tags=_[73]} +_[54]={text="4",tags=_[72]} +_[53]={text="3",tags=_[71]} +_[52]={text="2",tags=_[70]} +_[51]={text="1",tags=_[69]} +_[50]={text="0",tags=_[68]} +_[49]={text="return in loop:",tags=_[67]} +_[48]={text="10",tags=_[66]} +_[47]={text="9",tags=_[65]} +_[46]={text="8",tags=_[64]} +_[45]={text="7",tags=_[63]} +_[44]={text="6",tags=_[62]} +_[43]={text="5",tags=_[61]} +_[42]={text="4",tags=_[60]} +_[41]={text="3",tags=_[59]} +_[40]={text="2",tags=_[58]} +_[39]={text="1",tags=_[57]} +_[38]={text="0",tags=_[56]} +_[37]={_[55]} +_[36]={_[54]} +_[35]={_[53]} +_[34]={_[52]} +_[33]={_[51]} +_[32]={_[50]} +_[31]={_[49]} +_[30]={_[48]} +_[29]={_[47]} +_[28]={_[46]} +_[27]={_[45]} +_[26]={_[44]} +_[25]={_[43]} +_[24]={_[42]} +_[23]={_[41]} +_[22]={_[40]} +_[21]={_[39]} +_[20]={_[38]} +_[19]={"return"} +_[18]={"text",_[37]} +_[17]={"text",_[36]} +_[16]={"text",_[35]} +_[15]={"text",_[34]} +_[14]={"text",_[33]} +_[13]={"text",_[32]} +_[12]={"text",_[31]} +_[11]={"text",_[30]} +_[10]={"text",_[29]} +_[9]={"text",_[28]} +_[8]={"text",_[27]} +_[7]={"text",_[26]} +_[6]={"text",_[25]} +_[5]={"text",_[24]} +_[4]={"text",_[23]} +_[3]={"text",_[22]} +_[2]={"text",_[21]} +_[1]={"text",_[20]} +return {_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13],_[14],_[15],_[16],_[17],_[18],_[19]} +--[[ +{ "text", { { + tags = {}, + text = "0" + } } } +{ "text", { { + tags = {}, + text = "1" + } } } +{ "text", { { + tags = {}, + text = "2" + } } } +{ "text", { { + tags = {}, + text = "3" + } } } +{ "text", { { + tags = {}, + text = "4" + } } } +{ "text", { { + tags = {}, + text = "5" + } } } +{ "text", { { + tags = {}, + text = "6" + } } } +{ "text", { { + tags = {}, + text = "7" + } } } +{ "text", { { + tags = {}, + text = "8" + } } } +{ "text", { { + tags = {}, + text = "9" + } } } +{ "text", { { + tags = {}, + text = "10" + } } } +{ "text", { { + tags = {}, + text = "return in loop:" + } } } +{ "text", { { + tags = {}, + text = "0" + } } } +{ "text", { { + tags = {}, + text = "1" + } } } +{ "text", { { + tags = {}, + text = "2" + } } } +{ "text", { { + tags = {}, + text = "3" + } } } +{ "text", { { + tags = {}, + text = "4" + } } } +{ "text", { { + tags = {}, + text = "5" + } } } +{ "return" } +]]-- \ No newline at end of file