1
0
Fork 0
mirror of https://github.com/Reuh/anselme.git synced 2025-10-27 16:49:31 +00:00

Decorator system simplification, removed paragraph decorators, added function decorators

This commit is contained in:
Étienne Fildadut 2021-04-23 17:13:39 +02:00
parent 6f564ea0e2
commit 0171d92352
16 changed files with 265 additions and 290 deletions

View file

@ -3,7 +3,7 @@ Anselme
The overengineered dialog scripting system in pure Lua.
**Has been rewritten recently, doc is still WIP**
**Has been rewritten recently, doc and language are still WIP**
Purpose
-------
@ -90,7 +90,7 @@ Another line.
When executing a piece of Anselme code, it will not directly modify the global state (i.e. the values of variables used by every script), but only locally, in this execution.
Right after reaching a checkpoint (line or decorator), Anselme will merge the local state with the global one, i.e., make every change accessible to other scripts.
Right after reaching a checkpoint line, Anselme will merge the local state with the global one, i.e., make every change accessible to other scripts.
```
$ main
@ -296,19 +296,69 @@ And this is more text, in a different event.
### Line decorators
Every line can also be followed with decorators, which are appended at the end of the line and affect its behaviour.
Every line can also be followed with decorators, which are appended at the end of the line and affect its behaviour. Decorators are just syntaxic sugar to make some common operations simpler to write.
* `~`: condition decorator. Same as an condition line, behaving as if this line was it sole child. Typically used to conditionally execute line. Does not affect following else-conditions.
* `§`: checkpoint decorator. Same as a checkpoint line, behaving as if this line was it sole child.
* `#`: tag decorator. Same as a tag line, behaving as if this line was it sole child.
* `~`: condition decorator. Same as an condition line, behaving as if this line was it sole child. Typically used to conditionally execute line.
```
$ fn
run this line only once ~ 👁️
```
is equivalent to:
```
$ fn
~ 👁️
run this line only once
```
* `#`: tag decorator. Same as a tag line, behaving as if this line was it sole child.
```
tagged # 42
```
is equivalent to:
```
# 42
tagged
```
* `$`: function decorator. Same as a function line, behaving as if this line was it sole child, but also run the function.
```
text $ f
```
is equivalent to:
```
~ f
$ f
text
```
This is typically used for immediatletly running functions when defining them, for example for a looping choice :
```
~$ loop
> Loop
@loop
> Exit
```
is equivalent to (since empty condition is assumed true):
```
$ loop
> Loop
@loop
> Exit
~ loop
```
### Text interpolation
Text and choice lines allow for arbitrary text. Expression can be evaluated and inserted into the text as the line is executed by enclosing the [expression](#expressions) into brackets.
@ -368,7 +418,7 @@ Valid identifiers must be at least 1 caracters long and can contain anything exc
When defining an identifier (using a function, checkpoint or variable delcaration line), it will be defined into the current namespace (defined by the parent function/checkpoint). When evaluating an expression, Anselme will look for variables into the current line's namespace, then go up a level if it isn't found, and so on.
In practise, this means you have to use the "genealogy" of the variable to refer to it from a line not in it indentation block:
In practise, this means you have to use the "genealogy" of the variable to refer to it from a line not in the same namespace:
```
$ fn1

View file

@ -99,18 +99,9 @@ local function eval(state, exp)
end
-- anselme function
if type(fn.value) == "table" then
-- checkpoint & checkpoint decorator
if fn.value.type == "checkpoint" or fn.value.checkpoint then
local r, e
if fn.value.type == "checkpoint" then
r, e = run(state, fn.value.child, not exp.explicit_call)
-- checkpoint decorators: run single line or resume from it.
-- checkpoint & seen variables will be updated from the interpreter usual checkpoint-reaching code.
elseif exp.explicit_call then
r, e = run(state, fn.value.parent_block, false, fn.value.parent_position, fn.value.parent_position)
else
r, e = run(state, fn.value.parent_block, true, fn.value.parent_position)
end
-- checkpoint
if fn.value.type == "checkpoint" then
local r, e = run(state, fn.value.child, not exp.explicit_call)
if not r then return r, e end
return r
-- function

View file

@ -57,29 +57,25 @@ local run_block
local function run_line(state, line)
-- store line
state.interpreter.running_line = line
-- condition decorator
local skipped = false
if line.condition then
local v, e = eval(state, line.condition)
if not v then return v, ("%s; at %s"):format(e, line.source) end
skipped = not truthy(v)
-- if line intend to push an event, flush buffer it it's a different event
if line.push_event and state.interpreter.event_buffer and state.interpreter.event_type ~= line.push_event then
local v, e = run_line(state, { source = line.source, type = "flush_events" })
if e then return v, e end
if v then return v end
end
if not skipped then
-- tag decorator
if line.tag then
local v, e = eval(state, line.tag)
if not v then return v, ("%s; in tag decorator at %s"):format(e, line.source) end
tags:push(state, v)
end
-- if line intend to push an event, flush buffer it it's a different event
if line.push_event and state.interpreter.event_buffer and state.interpreter.event_type ~= line.push_event then
local v, e = run_line(state, { source = line.source, type = "flush_events" })
-- line types
if line.type == "condition" 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
if truthy(v) then
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
end
-- line types
if line.type == "condition" then
line.parent_block.last_condition_success = nil
elseif line.type == "else-condition" then
if not line.parent_block.last_condition_success then
local v, e = eval(state, line.expression)
if not v then return v, ("%s; at %s"):format(e, line.source) end
if truthy(v) then
@ -88,85 +84,68 @@ local function run_line(state, line)
if e then return v, e end
if v then return v end
end
elseif line.type == "else-condition" then
if not line.parent_block.last_condition_success then
local v, e = eval(state, line.expression)
if not v then return v, ("%s; at %s"):format(e, line.source) end
if truthy(v) then
line.parent_block.last_condition_success = true
v, e = run_block(state, line.child)
end
elseif line.type == "choice" then
local t, er = eval_text(state, line.text)
if not t then return t, er end
table.insert(state.interpreter.choice_available, {
tags = tags:current(state),
block = line.child
})
write_event(state, "choice", t)
elseif line.type == "tag" then
local v, e = eval(state, line.expression)
if not v then return v, ("%s; at %s"):format(e, line.source) end
tags:push(state, v)
v, e = run_block(state, line.child)
tags:pop(state)
if e then return v, e end
if v then return v end
elseif line.type == "return" then
local v, e = eval(state, line.expression)
if not v then return v, ("%s; at %s"):format(e, line.source) end
return v
elseif line.type == "text" then
local t, er = eval_text(state, line.text)
if not t then return t, ("%s; at %s"):format(er, line.source) end
write_event(state, "text", t)
elseif line.type == "flush_events" then
while state.interpreter.event_buffer do
local type, buffer = state.interpreter.event_type, state.interpreter.event_buffer
state.interpreter.event_type = nil
state.interpreter.event_buffer = nil
-- yield
coroutine.yield(type, buffer)
-- run choice
if type == "choice" then
local sel = state.interpreter.choice_selected
state.interpreter.choice_selected = nil
if not sel or sel < 1 or sel > #state.interpreter.choice_available then
return nil, "invalid choice"
else
local choice = state.interpreter.choice_available[sel]
state.interpreter.choice_available = {}
tags:push_lua_no_merge(state, choice.tags)
local v, e = run_block(state, choice.block)
tags:pop(state)
if e then return v, e end
if v then return v end
-- discard return value from choice block as the execution is delayed until an event flush
-- and we don't want to stop the execution of another function unexpectedly
end
end
elseif line.type == "choice" then
local t, er = eval_text(state, line.text)
if not t then return t, er end
table.insert(state.interpreter.choice_available, {
tags = tags:current(state),
block = line.child
})
write_event(state, "choice", t)
elseif line.type == "tag" then
local v, e = eval(state, line.expression)
if not v then return v, ("%s; at %s"):format(e, line.source) end
tags:push(state, v)
v, e = run_block(state, line.child)
tags:pop(state)
if e then return v, e end
if v then return v end
elseif line.type == "return" then
local v, e = eval(state, line.expression)
if not v then return v, ("%s; at %s"):format(e, line.source) end
return v
elseif line.type == "text" then
local t, er = eval_text(state, line.text)
if not t then return t, ("%s; at %s"):format(er, line.source) end
write_event(state, "text", t)
elseif line.type == "flush_events" then
while state.interpreter.event_buffer do
local type, buffer = state.interpreter.event_type, state.interpreter.event_buffer
state.interpreter.event_type = nil
state.interpreter.event_buffer = nil
-- yield
coroutine.yield(type, buffer)
-- run choice
if type == "choice" then
local sel = state.interpreter.choice_selected
state.interpreter.choice_selected = nil
if not sel or sel < 1 or sel > #state.interpreter.choice_available then
return nil, "invalid choice"
else
local choice = state.interpreter.choice_available[sel]
state.interpreter.choice_available = {}
tags:push_lua_no_merge(state, choice.tags)
local v, e = run_block(state, choice.block)
tags:pop(state)
if e then return v, e end
-- discard return value from choice block as the execution is delayed until an event flush
-- and we don't want to stop the execution of another function unexpectedly
end
end
end
elseif line.type ~= "checkpoint" then
return nil, ("unknown line type %q; at %s"):format(line.type, line.source)
end
-- tag decorator
if line.tag then
tags:pop(state)
end
-- checkpoint decorator and line
if line.checkpoint then
state.variables[line.namespace.."👁️"] = {
type = "number",
value = state.variables[line.namespace.."👁️"].value + 1
}
state.variables[line.parent_function.namespace.."🏁"] = {
type = "string",
value = line.name
}
merge_state(state)
end
elseif line.type == "checkpoint" then
state.variables[line.namespace.."👁️"] = {
type = "number",
value = state.variables[line.namespace.."👁️"].value + 1
}
state.variables[line.parent_function.namespace.."🏁"] = {
type = "string",
value = line.name
}
merge_state(state)
else
return nil, ("unknown line type %q; at %s"):format(line.type, line.source)
end
end
@ -196,9 +175,9 @@ run_block = function(state, block, resume_from_there, i, j)
i = i + 1
end
-- if we are exiting a checkpoint block, mark it as ran and update checkpoint
-- (when resuming from a checkpoint, execution is resumed from inside the checkpoint, the line.checkpoint check in run_line is never called)
-- (when resuming from a checkpoint, execution is resumed from inside the checkpoint, the line.type=="checkpoint" check in run_line is never called)
-- (and we want this to be done after executing the checkpoint block anyway)
if block.parent_line and block.parent_line.checkpoint then
if block.parent_line and block.parent_line.type == "checkpoint" then
local parent_line = block.parent_line
state.variables[parent_line.namespace.."👁️"] = {
type = "number",
@ -227,7 +206,7 @@ run_block = function(state, block, resume_from_there, i, j)
elseif parent_line.type == "condition" or parent_line.type == "else-condition" then
parent_line.parent_block.last_condition_success = true
end
if parent_line.type == "tag" or parent_line.tag then
if parent_line.type == "tag" then
tags:pop(state)
end
local v, e = run_block(state, parent_line.parent_block, resume_from_there, parent_line.parent_position+1)
@ -252,11 +231,6 @@ local function run(state, block, resume_from_there, i, j)
if not v then return v, ("%s; at %s"):format(e, parent_line.source) end
table.insert(tags_to_add, v)
end
if parent_line.tag then
local v, e = eval(state, parent_line.tag)
if not v then return v, ("%s; in tag decorator at %s"):format(e, parent_line.source) end
table.insert(tags_to_add, v)
end
parent_line = parent_line.parent_block.parent_line
end
-- re-add tag in desceding order

View file

@ -6,27 +6,6 @@ local parse_text
local function parse(state)
for _, l in ipairs(state.queued_lines) do
local line, namespace = l.line, l.namespace
-- decorators
if line.condition then
if line.condition:match("[^%s]") then
local exp, rem = expression(line.condition, state, namespace)
if not exp then return nil, ("%s; at %s"):format(rem, line.source) end
if rem:match("[^%s]") then return nil, ("expected end of expression before %q in condition decorator; at %s"):format(rem, line.source) end
line.condition = exp
else
line.condition = nil
end
end
if line.tag then
if line.tag:match("[^%s]") then
local exp, rem = expression(line.tag, state, namespace)
if not exp then return nil, ("%s; at %s"):format(rem, line.source) end
if rem:match("[^%s]") then return nil, ("expected end of expression before %q in condition decorator; at %s"):format(rem, line.source) end
line.tag = exp
else
line.tag = nil
end
end
-- expressions
if line.expression then
local exp, rem = expression(line.expression, state, namespace)

View file

@ -15,79 +15,6 @@ local function parse_line(line, state, namespace)
r.remove_from_block_ast = true
return r
end
-- decorators
while l:match("^..+[~#]") or l:match("^..+§") do
-- condition
if l:match("^..+%~.-$") then
local expr
l, expr = l:match("^(.-)%s*%~(.-)$")
r.condition = expr
-- checkpoint
elseif l:match("^..+§.-$") then
-- get identifier
local name
l, name = l:match("^(.-)%s*§(.-)$")
local identifier, rem = name:match("^("..identifier_pattern..")(.-)$")
if not identifier then return nil, ("no valid identifier in checkpoint decorator %q; at %s"):format(identifier, line.source) end
-- format identifier
local fqm = ("%s%s"):format(namespace, format_identifier(identifier))
-- get alias
if rem:match("^%:") then
local content = rem:sub(2)
local alias, rem2 = content:match("^("..identifier_pattern..")(.-)$")
if not alias then return nil, ("expected an identifier in alias in checkpoint decorator, but got %q; at %s"):format(content, line.source) end
if rem2:match("[^%s]") then return nil, ("expected end-of-line after identifier in alias in checkpoint decorator, but got %q; at %s"):format(rem2, line.source) end
-- format alias
local aliasfqm = ("%s%s"):format(namespace, format_identifier(alias))
-- define alias
if state.aliases[aliasfqm] ~= nil and state.aliases[aliasfqm] ~= fqm then
return nil, ("trying to define alias %q for variable %q, but already exist and refer to different variable %q; at %s"):format(aliasfqm, fqm, state.aliases[aliasfqm], line.source)
end
state.aliases[aliasfqm] = fqm
elseif rem:match("[^%s]") then
return nil, ("expected end-of-line after identifier in checkpoint decorator, but got %q; at %s"):format(rem, line.source)
end
-- define checkpoint
namespace = fqm.."."
r.checkpoint = true
r.parent_function = true
r.namespace = fqm.."."
r.name = fqm
if not state.functions[fqm] then
state.functions[fqm] = {
{
arity = 0,
value = r
}
}
if not state.variables[fqm..".👁️"] then
state.variables[fqm..".👁️"] = {
type = "number",
value = 0
}
end
-- define alias for 👁️
local seen_alias = state.builtin_aliases["👁️"]
if seen_alias then
local alias = ("%s.%s"):format(fqm, seen_alias)
if state.aliases[alias] ~= nil and state.aliases[alias] then
return nil, ("trying to define alias %q for variable %q, but already exist and refer to different variable %q; at %s"):format(alias, fqm..".👁️", state.aliases[alias], line.source)
end
state.aliases[alias] = fqm..".👁️"
end
else
table.insert(state.functions[fqm], {
arity = 0,
value = r
})
end
-- tag
elseif l:match("^..+%#.-$") then
local expr
l, expr = l:match("^(.-)%s*%#(.-)$")
r.tag = expr
end
end
-- else-condition & condition
if l:match("^~~?") then
r.type = l:match("^~~") and "else-condition" or "condition"
@ -169,7 +96,6 @@ local function parse_line(line, state, namespace)
end
-- store parent function and run checkpoint when line is read
if r.type == "checkpoint" then
r.checkpoint = true
r.parent_function = true
end
-- don't keep function node in block AST
@ -336,52 +262,78 @@ end
-- * nil, err: in case of error
local function parse_block(indented, state, namespace, parent_function)
local block = { type = "block" }
local lastLine -- last line AST
for i, l in ipairs(indented) do
for _, l in ipairs(indented) do
-- parsable line
if l.content then
local ast, err = parse_line(l, state, namespace)
if err then return nil, err end
lastLine = ast
-- store parent function
if ast.parent_function then ast.parent_function = parent_function end
-- add to block AST
if not ast.remove_from_block_ast then
ast.parent_block = block
-- add ast node
ast.parent_position = #block+1
if ast.replace_with then
if indented[i+1].content then
table.insert(indented, i+1, { content = ast.replace_with, source = l.source })
else
table.insert(indented, i+2, { content = ast.replace_with, source = l.source }) -- if line has children
end
else
table.insert(block, ast)
end
end
-- add child
if ast.child then ast.child = { type = "block", parent_line = ast } end
-- queue in expression evalution
table.insert(state.queued_lines, { namespace = ast.namespace or namespace, line = ast })
-- indented (ignore block comments)
elseif lastLine.type ~= "comment" then
if not lastLine.child then
return nil, ("line %s (%s) can't have children"):format(lastLine.source, lastLine.type)
local ast, err = parse_line(l, state, namespace)
if err then return nil, err end
-- store parent function
if ast.parent_function then ast.parent_function = parent_function end
-- add to block AST
if not ast.remove_from_block_ast then
ast.parent_block = block
-- add ast node
ast.parent_position = #block+1
table.insert(block, ast)
end
-- add child
if ast.child then ast.child = { type = "block", parent_line = ast } end
-- queue in expression evalution
table.insert(state.queued_lines, { namespace = ast.namespace or namespace, line = ast })
-- indented block (ignore block comments)
if l.children and ast.type ~= "comment" then
if not ast.child then
return nil, ("line %s (%s) can't have children"):format(ast.source, ast.type)
else
local r, e = parse_block(l, state, lastLine.namespace or namespace, lastLine.type == "function" and lastLine or parent_function)
local r, e = parse_block(l.children, state, ast.namespace or namespace, ast.type == "function" and ast or parent_function)
if not r then return r, e end
r.parent_line = lastLine
lastLine.child = r
r.parent_line = ast
ast.child = r
end
end
end
return block
end
--- returns the nested list of lines {content="", line=1}, grouped by indentation
-- returns new_indented
local function transform_indented(indented)
local i = 1
while i <= #indented do
local l = indented[i]
-- condition decorator
if l.content:match("^.-%s*[^~]%~[^#~$]-$") then
local decorator
l.content, decorator = l.content:match("^(..-)%s*(%~[^#~$]-)$")
indented[i] = { content = decorator, source = l.source, children = { l } }
-- tag decorator
elseif l.content:match("^..-%s*%#[^#~$]-$") then
local decorator
l.content, decorator = l.content:match("^(..-)%s*(%#[^#~$]-)$")
indented[i] = { content = decorator, source = l.source, children = { l } }
-- function decorator
elseif l.content:match("^..-%s*%$[^#~$]-$") then
local name
l.content, name = l.content:match("^(..-)%s*%$([^#~$]-)$")
indented[i] = { content = "~"..name, source = l.source }
table.insert(indented, i+1, { content = "$"..name, source = l.source, children = { l } })
i = i + 1 -- $ line should not contain any decorator anymore
else
i = i + 1 -- only increment when no decorator, as there may be several decorators per line
end
-- indented block
if l.children then
transform_indented(l.children)
end
end
return indented
end
--- returns the nested list of lines {content="", line=1, children={lines...} or nil}, parsing indentation
-- multiple empty lines are merged
-- * list, last line
-- * list, last line, insert_empty_line: in case of success
-- * nil, err: in case of error
local function parse_indent(lines, source, i, indentLevel, insert_empty_line)
i = i or 1
indentLevel = indentLevel or 0
@ -396,9 +348,14 @@ local function parse_indent(lines, source, i, indentLevel, insert_empty_line)
end
table.insert(indented, { content = line, source = ("%s:%s"):format(source, i) })
elseif #indent > indentLevel then
local t
t, i, insert_empty_line = parse_indent(lines, source, i, #indent, insert_empty_line)
table.insert(indented, t)
if #indented == 0 then
return nil, ("unexpected indentation; at %s:%s"):format(source, i)
else
local t
t, i, insert_empty_line = parse_indent(lines, source, i, #indent, insert_empty_line)
if not t then return nil, i end
indented[#indented].children = t
end
else
return indented, i-1, insert_empty_line
end
@ -426,17 +383,19 @@ end
local function parse(state, s, name, source)
-- parse lines
local lines = parse_lines(s)
local indented = parse_indent(lines, source or name)
local indented, e = parse_indent(lines, source or name)
if not indented then return nil, e end
-- wrap in named function if neccessary
if name ~= "" then
if not name:match("^"..identifier_pattern.."$") then
return nil, ("invalid function name %q"):format(name)
end
indented = {
{ content = "$ "..name, source = ("%s:%s"):format(source or name, 0) },
indented
{ content = "$ "..name, source = ("%s:%s"):format(source or name, 0), children = indented },
}
end
-- transform ast
indented = transform_indented(indented)
-- parse
local root, err = parse_block(indented, state, "")
if not root then return nil, err end

View file

@ -195,6 +195,7 @@ else
if args["write-new"] and e:match("No such file") then
write_result(filebase, result)
print("Written result file for "..filebase)
success = success + 1
elseif not args.silent then
print("> "..namespace)
print(e)

View file

@ -0,0 +1,3 @@
a.👁️: {a.👁️} $ a
~ a()

View file

@ -0,0 +1,9 @@
$ f
ko
a.👁️: {a.👁️} $ a
ok
~ f.a
In function:
~ f

View file

@ -0,0 +1,37 @@
local _={}
_[15]={}
_[14]={}
_[13]={}
_[12]={}
_[11]={}
_[10]={data="ok",tags=_[15]}
_[9]={data="a.\240\159\145\129\239\184\143: 1",tags=_[14]}
_[8]={data="ko",tags=_[13]}
_[7]={data="In function:",tags=_[12]}
_[6]={data="a.\240\159\145\129\239\184\143: 0",tags=_[11]}
_[5]={_[7],_[8],_[9],_[10]}
_[4]={_[6]}
_[3]={"return"}
_[2]={"text",_[5]}
_[1]={"text",_[4]}
return {_[1],_[2],_[3]}
--[[
{ "text", { {
data = "a.👁️: 0",
tags = {}
} } }
{ "text", { {
data = "In function:",
tags = {}
}, {
data = "ko",
tags = {}
}, {
data = "a.👁️: 1",
tags = {}
}, {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -0,0 +1 @@
a.👁️: {a.👁️} $ a

View file

@ -1,6 +1,6 @@
local _={}
_[5]={}
_[4]={data="a.\240\159\145\129\239\184\143: 0",tags=_[5]}
_[4]={tags=_[5],data="a.\240\159\145\129\239\184\143: 0"}
_[3]={_[4]}
_[2]={"return"}
_[1]={"text",_[3]}

View file

@ -1,3 +0,0 @@
a.👁️: {a.👁️} § a
~ a()

View file

@ -1,6 +0,0 @@
$ f
ko
a.👁️: {a.👁️} § a
ok
~ f.a

View file

@ -1,19 +0,0 @@
local _={}
_[7]={}
_[6]={}
_[5]={data="ok",tags=_[7]}
_[4]={data="a.\240\159\145\129\239\184\143: 0",tags=_[6]}
_[3]={_[4],_[5]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
data = "a.👁️: 0",
tags = {}
}, {
data = "ok",
tags = {}
} } }
{ "return" }
]]--

View file

@ -1 +0,0 @@
a.👁️: {a.👁️} § a