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

Changed a few things

* Integrated # and ~ decorators into the expression system. Created associated operators.
* # and ~ decorators only affect their current line. That's more useful...
* Fix our priority system to evaluate left-to-right instead of right-to-left (if there was a reason why I did it this way initially, I don't remember it so ¯\_(ツ)_/¯)
* a lotta internal changes

Various other small adjustments, see the diff of REFERENCE.md for details.
This commit is contained in:
Étienne Fildadut 2021-11-28 01:43:54 +01:00
parent 14d348bad9
commit f2e74c94c9
31 changed files with 894 additions and 343 deletions

View file

@ -156,7 +156,7 @@ There's different types of lines, depending on their first character(s) (after i
This is.
```
* `>`: 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.
* `>`: 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.
```
$ f
@ -168,6 +168,18 @@ $ 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.
```
(Conditionnaly executing a line)
$ fn
> show this choice only once ~ 👁️
(Tagging a single line)
> tagged # 42
not tagged
```
* `$`: function line. Followed by an [identifier](#identifiers), then eventually an [alias](#aliases), and eventually a parameter list. Define a function using its children as function body. Also define a new namespace for its children (using the function name if it has no arguments, or a unique name otherwise).
The function body is not executed when the line is reached; it must be explicitely called in an expression. See [expressions](#function-calls) to see the different ways of calling a function.
@ -223,7 +235,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:
```
$ /(a::string, b::string)
@ -323,7 +335,7 @@ $ f
* empty line: flush the event buffer, i.e., if there are any pending lines of text or choices, send them to your game. See [Event buffer](#event-buffer). This line always keep the same identation as the last non-empty line, so you don't need to put invisible whitespace on an empty-looking line. Is also automatically added at the end of a file.
* regular text: write some text into the [event buffer](#event-buffer). Support [text interpolation](#text-interpolation).
* regular text: write some text into the [event buffer](#event-buffer). Support [text interpolation](#text-interpolation). Support [escape codes](#escape-codes).
```
Hello,
@ -332,37 +344,20 @@ this is some text.
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. 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.
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)
$ 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.
```
(Tagging a single line)
tagged # 42
```
is equivalent to:
### Line decorators
```
# 42
tagged
```
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.
* `$`: function decorator. Same as a function line, behaving as if this line was it sole child, but also run the function.
@ -428,6 +423,17 @@ Hello {f}
:b = "{f}"
```
Text interpolation in text and choices lines also support subtexts: this will process text in squares brackets `[]` in the same way as a regular text line.
```
Hello [world].
(Typically used to tag part of a line in a compact manner)
Hello [world#5]
> Hello [world#5]
```
### Events
Anselme need to give back control to the game at some point. This is done through events: the interpreter regularly yield its coroutine and returns a bunch of data to your game. This is the "event", it is what we call whatever Anselme sends back to your game.
@ -483,14 +489,14 @@ By default, some processing is done on the event buffer before sending it to you
* strip trailing spaces: will remove any space caracters at the end of the text (for text event), or at the end of each choice (for choice event).
```
(There is a space between the text and the tag decorator that would be included in the text event otherwise.)
(There is a space between the text and the tag expression that would be included in the text event otherwise.)
Some text # tag
```
* strip duplicate spaces: will remove any duplicated space caracters between each element that constitute the text (for text event), or for each choice (for choice event).
```
(There is a space between the text and the tag decorator; but there is a space as well after the text interpolation in the last line. The two spaces are converted into a single space (the space will belong to the first text element, i.e. the "text " element).)
(There is a space between the text and the tag expression; but there is a space as well after the text interpolation in the last line. The two spaces are converted into a single space (the space will belong to the first text element, i.e. the "text " element).)
$ f
text # tag
@ -577,7 +583,7 @@ Default types are:
* `number`: a number (double). Can be defined using the forms `42`, `.42`, `42.42`.
* `string`: a string. Can be defined between double quotes `"string"`. Support [text interpolation](#text-interpolation). Support the escape codes `\\` for `\`, `\"` for `"`, `\n` for a newline and `\t` for a tabulation.
* `string`: a string. Can be defined between double quotes `"string"`. Support [text interpolation](#text-interpolation). Support [escape codes](#escape-codes).
* `list`: a list of values. Types can be mixed. Can be defined between square brackets and use comma as a separator '[1,2,3,4]'.
@ -609,6 +615,19 @@ How conservions are handled from Lua to Anselme:
* `boolean` -> `number`, 0 for false, 1 for true.
#### Escapes codes
These can be used to represent some caracters in string and other text elements that would otherwise be difficult to express due to conflicts with Anselme syntax.
* `\{` for `{`
* `\~` for `~`
* `\#` for `#`
* `\$` for `$`
* `\\` for `\`
* `\"` for `"`
* `\n` for a newline
* `\t` for a tabulation
#### Truethness
Only `0` and `nil` are false. Everything else is considered true.
@ -696,7 +715,7 @@ $ f(a, b, c)
{f(1,2,3)} = {f(c=3,b=2,a=1)} = {f(1,2,c=3)}
```
Anselme actually treat argument list are regular lists; named arguments are actually pairs.
Anselme actually treat argument list are regular lists; named arguments are actually pairs. Arguments are evaluated left-to-right.
This means that pairs can't be passed directly as arguments to a function (as they will be considered named arguments). If you want to use pairs, always wrap them in a list.
@ -792,7 +811,7 @@ Please also be aware that when resuming from a checkpoint, Anselme will try to r
* if the checkpoint is in a condition block, it will assume the condition was true (but will not re-evaluate it)
* if the checkpoint is in a choice block, it will assume this choice was selected (but will not re-evaluate any of the choices from the same choice group)
* will try to re-add every tag from parent lines; this require Anselme to re-evaluate every tag line and decorator that's a parent of the checkpoint in the function. Be careful if your tag expressions have side-effects.
* will try to re-add every tag from parent lines; this require Anselme to re-evaluate every tag lines that are a parent of the checkpoint in the function. Be careful if your tag expressions have side-effects.
##### Operator priority
@ -802,7 +821,7 @@ From lowest to highest priority:
;
:= += -= //= /= *= %= ^=
,
| &
| & ~ #
!= == >= <= < >
+ -
* // / %
@ -812,6 +831,8 @@ unary -, unary !
.
```
A series of operators with the same priority are evaluated left-to-right.
#### Operators
Built-in operators:
@ -868,6 +889,10 @@ 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 and returns a, otherwise returns nil (lazy).
`a # b`: evaluates b, then evaluates a whith b added to the active tags.
`a(b)`: evaluate b (number), returns the value with this index in a (list). Use 1-based indexing. If b is a string, will search the first pair in the list with this string as its name. Operator is named `()`.
`{}(v)`: function called when formatting a value in a text interpolation for printing

View file

@ -6,11 +6,11 @@ local anselme = {
-- api is incremented a each update which may break Lua API compatibility
versions = {
save = 1,
language = 17,
language = 18,
api = 2
},
-- version is incremented at each update
version = 18,
version = 19,
--- currently running interpreter
running = nil
}

View file

@ -1,5 +1,32 @@
local atypes, ltypes
local eval
local eval, run_block
local function post_process_text(state, text)
-- remove trailing spaces
if state.feature_flags["strip trailing spaces"] then
local final = text[#text]
if final then
final.text = final.text:match("^(.-) *$")
if final.text == "" then
table.remove(text)
end
end
end
-- remove duplicate spaces
if state.feature_flags["strip duplicate spaces"] then
for i=1, #text-1 do
local a, b = text[i], text[i+1]
local na = #a.text:match(" *$")
local nb = #b.text:match("^ *")
if na > 0 and nb > 0 then -- remove duplicated spaces from second element first
b.text = b.text:match("^ *(.-)$")
end
if na > 1 then
a.text = a.text:match("^(.- ) *$")
end
end
end
end
local common
common = {
@ -150,12 +177,190 @@ common = {
else
return var.type
end
end,
--- tag management
tags = {
--- push new tags on top of the stack, from Anselme values
push = function(self, state, val)
local new = {}
-- copy
local last = self:current(state)
for k,v in pairs(last) do new[k] = v end
-- merge with new values
if val.type ~= "list" then val = { type = "list", value = { val } } end
for k, v in pairs(common.to_lua(val)) do new[k] = v end
-- add
table.insert(state.interpreter.tags, new)
return self:len(state)
end,
--- same but do not merge with last stack item
push_lua_no_merge = function(self, state, val)
table.insert(state.interpreter.tags, val)
return self:len(state)
end,
-- pop tag table on top of the stack
pop = function(self, state)
table.remove(state.interpreter.tags)
end,
--- return current lua tags table
current = function(self, state)
return state.interpreter.tags[#state.interpreter.tags] or {}
end,
--- returns length of tags stack
len = function(self, state)
return #state.interpreter.tags
end,
--- pop item until we reached desired stack length
-- try to prefer this to pop if possible, so in case we mess up the stack somehow it will restore the stack to a good state
-- (we may allow tag push/pop from the user side at some point TODO)
trim = function(self, state, len)
while #state.interpreter.tags > len do
self:pop(state)
end
end
},
--- event buffer management
-- i.e. only for text and choice events
events = {
--- add a new element to the event buffer
-- will flush if needed
-- returns true in case of success
-- returns nil, err in case of error
append = function(self, state, type, data)
if state.interpreter.event_capture_stack[type] then
local r, e = state.interpreter.event_capture_stack[type][#state.interpreter.event_capture_stack[type]](data)
if not r then return r, e end
else
local r, e = self:make_space_for(state, type)
if not r then return r, e end
if not state.interpreter.event_buffer then
state.interpreter.event_type = type
state.interpreter.event_buffer = {}
end
table.insert(state.interpreter.event_buffer, data)
end
return true
end,
--- add a new item in the last element (a list of elements) of the event buffer
-- will flush if needed
-- will use default or a new list if buffer is empty
-- returns true in case of success
-- returns nil, err in case of error
append_in_last = function(self, state, type, data, default)
local r, e = self:make_space_for(state, type)
if not r then return r, e end
if not state.interpreter.event_buffer then
r, e = self:append(state, type, default or {})
if not r then return r, e end
end
table.insert(state.interpreter.event_buffer[#state.interpreter.event_buffer], data)
return true
end,
--- start capturing events of a certain type
-- when an event of the type is appended, fn will be called with this event data
-- and the event will not be added to the event buffer
-- fn returns nil, err in case of error
push_capture = function(self, state, type, fn)
if not state.interpreter.event_capture_stack[type] then
state.interpreter.event_capture_stack[type] = {}
end
table.insert(state.interpreter.event_capture_stack[type], fn)
end,
--- stop capturing events of a certain type.
-- must be called after a push_capture
-- this is handled by a stack so nested capturing is allowed.
pop_capture = function(self, state, type)
table.remove(state.interpreter.event_capture_stack[type])
if #state.interpreter.event_capture_stack[type] == 0 then
state.interpreter.event_capture_stack[type] = nil
end
end,
-- flush event buffer if it's neccessary to push an event of the given type
-- returns true in case of success
-- returns nil, err in case of error
make_space_for = function(self, state, type)
if state.interpreter.event_buffer and state.interpreter.event_type ~= type and not state.interpreter.event_capture_stack[type] then
return self:flush(state)
end
return true
end,
--- flush events and send them to the game if possible
-- returns true in case of success
-- returns nil, err in case of error
flush = function(self, state)
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
state.interpreter.skip_choices_until_flush = nil
-- choice processing
local choices
if type == "choice" then
choices = {}
-- discard empty choices
for i=#buffer, 1, -1 do
if #buffer[i] == 0 then
table.remove(buffer, i)
end
end
-- extract some needed state data for each choice block
for _, c in ipairs(buffer) do
table.insert(choices, c._state)
c._state = nil
end
-- nervermind
if #choices == 0 then
return true
end
end
-- text & choice text content post processing
if type == "text" then
post_process_text(state, buffer)
end
if type == "choice" then
for _, c in ipairs(buffer) do
post_process_text(state, c)
end
end
-- yield event
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 > #choices then
return nil, "invalid choice"
else
local choice = choices[sel]
-- execute in expected tag & event capture state
local capture_state = state.interpreter.event_capture_stack
state.interpreter.event_capture_stack = {}
local i = common.tags:push_lua_no_merge(state, choice.tags)
local _, e = run_block(state, choice.block)
common.tags:trim(state, i-1)
state.interpreter.event_capture_stack = capture_state
if e then return nil, e end
-- we 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
return true
end
}
}
package.loaded[...] = common
local types = require((...):gsub("interpreter%.common$", "stdlib.types"))
atypes, ltypes = types.anselme, types.lua
eval = require((...):gsub("common$", "expression"))
run_block = require((...):gsub("common$", "interpreter")).run_block
return common

View file

@ -1,5 +1,5 @@
local expression
local to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable
local to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable, tags, eval_text_callback, events, flatten_list
local run
@ -54,21 +54,29 @@ local function eval(state, exp)
end
-- list
elseif exp.type == "list" then
local flat = flatten_list(exp)
local l = {}
local ast = exp
while ast.type == "list" do
local left, lefte = eval(state, ast.left)
if not left then return left, lefte end
table.insert(l, left)
ast = ast.right
for _, ast in ipairs(flat) do
local v, e = eval(state, ast)
if not v then return v, e end
table.insert(l, v)
end
local right, righte = eval(state, ast)
if not right then return right, righte end
table.insert(l, right)
return {
type = "list",
value = l
}
-- text: only triggered from choice/text lines
elseif exp.type == "text" then
local currentTags = tags:current(state)
local v, e = eval_text_callback(state, exp.text, function(text)
local v2, e2 = events:append(state, "text", { text = text, tags = currentTags })
if not v2 then return v2, e2 end
end)
if not v then return v, e end
return {
type = "nil",
value = nil
}
-- assignment
elseif exp.type == ":=" then
if exp.left.type == "variable" then
@ -113,6 +121,28 @@ local function eval(state, exp)
type = "number",
value = truthy(right) and 1 or 0
}
-- conditional
elseif exp.type == "~" then
local right, righte = eval(state, exp.right)
if not right then return right, righte end
if truthy(right) then
local left, lefte = eval(state, exp.left)
if not left then return left, lefte end
return left
end
return {
type = "nil",
value = nil
}
-- tag
elseif exp.type == "#" then
local right, righte = eval(state, exp.right)
if not right then return right, righte end
local i = tags:push(state, right)
local left, lefte = eval(state, exp.left)
tags:trim(state, i-1)
if not left then return left, lefte end
return left
-- variable
elseif exp.type == "variable" then
return get_variable(state, exp.name)
@ -390,7 +420,8 @@ end
package.loaded[...] = eval
run = require((...):gsub("expression$", "interpreter")).run
expression = require((...):gsub("interpreter%.expression$", "parser.expression"))
flatten_list = require((...):gsub("interpreter%.expression$", "parser.common")).flatten_list
local common = require((...):gsub("expression$", "common"))
to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable = common.to_lua, common.from_lua, common.eval_text, common.is_of_type, common.truthy, common.format, common.pretty_type, common.get_variable
to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable, tags, eval_text_callback, events = common.to_lua, common.from_lua, common.eval_text, common.is_of_type, common.truthy, common.format, common.pretty_type, common.get_variable, common.tags, common.eval_text_callback, common.events
return eval

View file

@ -1,202 +1,7 @@
local eval
local truthy, merge_state, to_lua, escape, get_variable, eval_text_callback
local truthy, merge_state, escape, get_variable, eval_text_callback, tags, events
local run_line, run_block
local function post_process_text(state, text)
-- remove trailing spaces
if state.feature_flags["strip trailing spaces"] then
local final = text[#text]
if final then
final.text = final.text:match("^(.-) *$")
if final.text == "" then
table.remove(text)
end
end
end
-- remove duplicate spaces
if state.feature_flags["strip duplicate spaces"] then
for i=1, #text-1 do
local a, b = text[i], text[i+1]
local na = #a.text:match(" *$")
local nb = #b.text:match("^ *")
if na > 0 and nb > 0 then -- remove duplicated spaces from second element first
b.text = b.text:match("^ *(.-)$")
end
if na > 1 then
a.text = a.text:match("^(.- ) *$")
end
end
end
end
--- tag management
local tags = {
--- push new tags on top of the stack, from Anselme values
push = function(self, state, val)
local new = {}
-- copy
local last = self:current(state)
for k,v in pairs(last) do new[k] = v end
-- merge with new values
if val.type ~= "list" then val = { type = "list", value = { val } } end
for k, v in pairs(to_lua(val)) do new[k] = v end
-- add
table.insert(state.interpreter.tags, new)
return self:len(state)
end,
--- same but do not merge with last stack item
push_lua_no_merge = function(self, state, val)
table.insert(state.interpreter.tags, val)
return self:len(state)
end,
-- pop tag table on top of the stack
pop = function(self, state)
table.remove(state.interpreter.tags)
end,
--- return current lua tags table
current = function(self, state)
return state.interpreter.tags[#state.interpreter.tags] or {}
end,
--- returns length of tags stack
len = function(self, state)
return #state.interpreter.tags
end,
--- pop item until we reached desired stack length
-- try to prefer this to pop if possible, so in case we mess up the stack somehow it will restore the stack to a good state
-- (we may allow tag push/pop from the user side at some point)
trim = function(self, state, len)
while #state.interpreter.tags > len do
self:pop(state)
end
end
}
--- event buffer management
-- i.e. only for text and choice events
local events = {
--- add a new element to the event buffer
-- will flush if needed
-- returns true in case of success
-- returns nil, err in case of error
append = function(self, state, type, data)
if state.interpreter.event_capture_stack[type] then
local r, e = state.interpreter.event_capture_stack[type][#state.interpreter.event_capture_stack[type]](data)
if not r then return r, e end
else
local r, e = self:make_space_for(state, type)
if not r then return r, e end
if not state.interpreter.event_buffer then
state.interpreter.event_type = type
state.interpreter.event_buffer = {}
end
table.insert(state.interpreter.event_buffer, data)
end
return true
end,
--- add a new item in the last element (a list of elements) of the event buffer
-- will flush if needed
-- will use default or a new list if buffer is empty
-- returns true in case of success
-- returns nil, err in case of error
append_in_last = function(self, state, type, data, default)
local r, e = self:make_space_for(state, type)
if not r then return r, e end
if not state.interpreter.event_buffer then
r, e = self:append(state, type, default or {})
if not r then return r, e end
end
table.insert(state.interpreter.event_buffer[#state.interpreter.event_buffer], data)
return true
end,
--- start capturing events of a certain type
-- when an event of the type is appended, fn will be called with this event data
-- and the event will not be added to the event buffer
-- fn returns nil, err in case of error
push_capture = function(self, state, type, fn)
if not state.interpreter.event_capture_stack[type] then
state.interpreter.event_capture_stack[type] = {}
end
table.insert(state.interpreter.event_capture_stack[type], fn)
end,
--- stop capturing events of a certain type.
-- must be called after a push_capture
-- this is handled by a stack so nested capturing is allowed.
pop_capture = function(self, state, type)
table.remove(state.interpreter.event_capture_stack[type])
if #state.interpreter.event_capture_stack[type] == 0 then
state.interpreter.event_capture_stack[type] = nil
end
end,
-- flush event buffer if it's neccessary to push an event of the given type
-- returns true in case of success
-- returns nil, err in case of error
make_space_for = function(self, state, type)
if state.interpreter.event_buffer and state.interpreter.event_type ~= type and not state.interpreter.event_capture_stack[type] then
return self:flush(state)
end
return true
end,
--- flush events and send them to the game if possible
-- returns true in case of success
-- returns nil, err in case of error
flush = function(self, state)
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
state.interpreter.skip_choices_until_flush = nil
-- extract some needed state data for each choice block
local choices
if type == "choice" then
choices = {}
for _, c in ipairs(buffer) do
table.insert(choices, c._state)
c._state = nil
end
end
-- text post processing
if type == "text" then
post_process_text(state, buffer)
end
if type == "choice" then
for _, c in ipairs(buffer) do
post_process_text(state, c)
end
end
-- yield event
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 > #choices then
return nil, "invalid choice"
else
local choice = choices[sel]
-- execute in expected tag & event capture state
local capture_state = state.interpreter.event_capture_stack
state.interpreter.event_capture_stack = {}
local i = tags:push_lua_no_merge(state, choice.tags)
local _, e = run_block(state, choice.block)
tags:trim(state, i-1)
state.interpreter.event_capture_stack = capture_state
if e then return nil, e end
-- we 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
return true
end
}
-- returns var in case of success and there is a return
-- return nil in case of success and there is no return
-- return nil, err in case of error
@ -236,10 +41,7 @@ run_line = function(state, line)
local v2, e2 = events:append_in_last(state, "choice", event, { _state = choice_block_state })
if not v2 then return v2, e2 end
end)
v, e = eval_text_callback(state, line.text, function(text)
local v2, e2 = events:append_in_last(state, "choice", { text = text, tags = currentTags }, { _state = choice_block_state })
if not v2 then return v2, e2 end
end)
v, e = eval(state, line.text)
events:pop_capture(state, "text")
if not v then return v, ("%s; at %s"):format(e, line.source) end
elseif line.type == "tag" then
@ -257,12 +59,7 @@ run_line = function(state, line)
elseif line.type == "text" then
local v, e = events:make_space_for(state, "text") -- do this before any evaluation start
if not v then return v, ("%s; in automatic event flush at %s"):format(e, line.source) end
local currentTags = tags:current(state)
v, e = eval_text_callback(state, line.text, function(text)
-- why you would want to send a non-text event from there, I have no idea, but I'm not going to stop you
local v2, e2 = events:append(state, "text", { text = text, tags = currentTags })
if not v2 then return v2, e2 end
end)
v, e = eval(state, line.text)
if not v then return v, ("%s; at %s"):format(e, line.source) end
elseif line.type == "flush_events" then
local v, e = events:flush(state)
@ -409,7 +206,7 @@ local interpreter = {
package.loaded[...] = interpreter
eval = require((...):gsub("interpreter$", "expression"))
local common = require((...):gsub("interpreter$", "common"))
truthy, merge_state, to_lua, get_variable, eval_text_callback = common.truthy, common.merge_state, common.to_lua, common.get_variable, common.eval_text_callback
truthy, merge_state, tags, get_variable, eval_text_callback, events = common.truthy, common.merge_state, common.tags, common.get_variable, common.eval_text_callback, common.events
escape = require((...):gsub("interpreter%.interpreter$", "parser.common")).escape
return interpreter

View file

@ -28,7 +28,8 @@ common = {
-- operators not included here:
-- * 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 & oprators: 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
-- * . operator: don't behave like regular functions either
";",
"!=", "==", ">=", "<=", "<", ">",
@ -45,7 +46,11 @@ common = {
["\\\\"] = "\\",
["\\\""] = "\"",
["\\n"] = "\n",
["\\t"] = "\t"
["\\t"] = "\t",
["\\~"] = "~",
["\\#"] = "#",
["\\$"] = "$", -- FIXME
["\\{"] = "{"
},
--- escape a string to be used as an exact match pattern
escape = function(str)
@ -116,38 +121,90 @@ common = {
flatten_list = function(list, t)
t = t or {}
if list.type == "list" then
table.insert(t, list.left)
common.flatten_list(list.right, t)
table.insert(t, 1, list.right)
common.flatten_list(list.left, t)
else
table.insert(t, list)
table.insert(t, 1, list)
end
return t
end,
-- parse interpolated expressions in a text
-- * list of strings and expressions
-- allow_subtext (bool) to enable or not [subtext] support
-- if allow_binops is given, if one of the caracters of allow_binops appear unescaped in the text, it will interpreter a binary operator expression
-- * returns a text expression, remaining (if the right expression stop before the end of the text)
-- if allow_binops is not given:
-- * returns a list of strings and expressions (text elements)
-- * nil, err: in case of error
parse_text = function(text, state, namespace)
parse_text = function(text, state, namespace, allow_binops, allow_subtext, in_subtext)
local l = {}
while text:match("[^%{]+") do
local t, e = text:match("^([^%{]*)(.-)$")
local text_exp
local delimiters = ""
if allow_binops then
text_exp = { type = "text", text = l }
delimiters = allow_binops
end
if allow_subtext then
delimiters = delimiters .. "%["
end
if in_subtext then
delimiters = delimiters .. "%]"
end
while text:match(("[^{%s]+"):format(delimiters)) do
local t, r = text:match(("^([^{%s]*)(.-)$"):format(delimiters))
-- text
if t ~= "" then table.insert(l, t) end
if t ~= "" then
-- handle \{ escape: skip to next { until it's not escaped
while t:match("\\$") and r:match(("^[{%s]"):format(delimiters)) do
local t2, r2 = r:match(("^([{%s][^{%s]*)(.-)$"):format(delimiters, delimiters))
t = t:match("^(.-)\\$") .. t2
r = r2
end
-- replace other escape codes
local escaped = t:gsub("\\.", common.string_escapes)
table.insert(l, escaped)
end
-- expr
if e:match("^{") then
local exp, rem = expression(e:gsub("^{", ""), state, namespace)
if r:match("^{") then
local exp, rem = expression(r:gsub("^{", ""), state, namespace)
if not exp then return nil, rem end
if not rem:match("^%s*}") then return nil, ("expected closing } at end of expression before %q"):format(rem) end
-- wrap in format() call
local variant, err = common.find_function_variant(state, namespace, "{}", exp, true)
local variant, err = common.find_function_variant(state, namespace, "{}", { type = "parentheses", expression = exp }, true)
if not variant then return variant, err end
-- add to text
table.insert(l, variant)
text = rem:match("^%s*}(.*)$")
-- start subtext
elseif allow_subtext and r:match("^%[") then
local exp, rem = common.parse_text(r:gsub("^%[", ""), state, namespace, allow_binops, allow_subtext, true)
if not exp then return nil, rem end
if not rem:match("^%]") then return nil, ("expected closing ] at end of subtext before %q"):format(rem) end
-- add to text
table.insert(l, exp)
text = rem:match("^%](.*)$")
-- end subtext
elseif in_subtext and r:match("^%]") then
if allow_binops then
return text_exp, r
else
break
end
end
return l
end
-- binop expression at the end of the text
elseif allow_binops and r:match(("^[%s]"):format(allow_binops)) then
local exp, rem = expression(r, state, namespace, nil, text_exp)
if not exp then return nil, rem end
return exp, rem
elseif r == "" then
break
else
error(("unexpected %q at end of text or string"):format(r))
end
end
if allow_binops then
return text_exp, ""
else
return l
end
end,
-- find compatible function variants from a fully qualified name
-- this functions does not guarantee that functions are fully compatible with the given arguments and only performs a pre-selection without the ones which definitely aren't

View file

@ -1,11 +1,11 @@
local identifier_pattern, format_identifier, find, escape, find_function_variant, parse_text, string_escapes
local identifier_pattern, format_identifier, find, escape, find_function_variant, parse_text
--- binop priority
local binops_prio = {
[1] = { ";" },
[2] = { ":=", "+=", "-=", "//=", "/=", "*=", "%=", "^=" },
[3] = { "," },
[4] = { "|", "&" },
[4] = { "|", "&", "~", "#" },
[5] = { "!=", "==", ">=", "<=", "<", ">" },
[6] = { "+", "-" },
[7] = { "*", "//", "/", "%" },
@ -70,12 +70,6 @@ local function expression(s, state, namespace, current_priority, operating_on)
-- parse interpolated expressions
local l, e = parse_text(d, state, namespace)
if not l then return l, e end
-- escape the string parts
for j, ls in ipairs(l) do
if type(ls) == "string" then
l[j] = ls:gsub("\\.", string_escapes)
end
end
return expression(r, state, namespace, current_priority, {
type = "string",
value = l
@ -191,7 +185,7 @@ local function expression(s, state, namespace, current_priority, operating_on)
else
-- binop
for prio, oplist in ipairs(binops_prio) do
if prio >= current_priority then
if prio > current_priority then
for _, op in ipairs(oplist) do
local escaped = escape(op)
if s:match("^"..escaped) then
@ -275,7 +269,7 @@ local function expression(s, state, namespace, current_priority, operating_on)
left = operating_on,
right = right
})
elseif op == "&" or op == "|" then
elseif op == "&" or op == "|" or op == "~" or op == "#" then
return expression(r, state, namespace, current_priority, {
type = op,
left = operating_on,
@ -287,8 +281,7 @@ local function expression(s, state, namespace, current_priority, operating_on)
local args = {
type = "list",
left = operating_on,
-- wrap in parentheses to avoid appending to argument list if right is a list
right = { type = "parentheses", expression = right }
right = right
}
local variant, err = find_function_variant(state, namespace, op, args, true)
if not variant then return variant, err end
@ -318,6 +311,6 @@ end
package.loaded[...] = expression
local common = require((...):gsub("expression$", "common"))
identifier_pattern, format_identifier, find, escape, find_function_variant, parse_text, string_escapes = common.identifier_pattern, common.format_identifier, common.find, common.escape, common.find_function_variant, common.parse_text, common.string_escapes
identifier_pattern, format_identifier, find, escape, find_function_variant, parse_text = common.identifier_pattern, common.format_identifier, common.find, common.escape, common.find_function_variant, common.parse_text
return expression

View file

@ -55,10 +55,11 @@ local function parse(state)
state.variables[line.fqm].value.expression = line.expression
end
end
-- text
-- text (text & choice lines)
if line.text then
local txt, err = parse_text(line.text, state, namespace)
if err then return nil, ("%s; at %s"):format(err, line.source) end
local txt, err = parse_text(line.text, state, namespace, "#~", true)
if not txt then return nil, ("%s; at %s"):format(err, line.source) end
if err:match("[^%s]") then return nil, ("expected end of expression in end-of-text expression before %q"):format(err) end
line.text = txt
end
state.queued_lines[i] = nil

View file

@ -325,18 +325,8 @@ local function transform_indented(indented)
if l.content:match("^%(") then
table.remove(indented, i)
else
-- condition decorator
if l.content:match("^.-[^~]%~[^#~$]-$") then
local decorator
l.content, decorator = l.content:match("^(..-)(%~[^#~$]-)$")
indented[i] = { content = decorator, source = l.source, children = { l } }
-- tag decorator
elseif l.content:match("^..-%#[^#~$]-$") then
local decorator
l.content, decorator = l.content:match("^(..-)(%#[^#~$]-)$")
indented[i] = { content = decorator, source = l.source, children = { l } }
-- function decorator
elseif l.content:match("^..-%$[^#~$]-$") then
if l.content:match("^.-[^\\]%$[^#~$]-$") then -- FIXME
local name
l.content, name = l.content:match("^(..-)%$([^#~$]-)$")
indented[i] = { content = "~"..name, source = l.source }

View file

@ -79,7 +79,7 @@ if args.help then
print("")
print("For script or game mode:")
print(" --lang code: load a language file")
print(" --save: print VM state at the end of the script")
print(" --save: print save data at the end of the script")
os.exit()
end

View file

@ -0,0 +1,35 @@
> a ~ 1
-> a
> b
-> b
~ choose(1)
> a ~ 1
-> a
> b
-> b
~ choose(2)
> a ~ 0
-> a
> b
-> b
~ choose(1)
> a
-> a
> b # 25
-> b
~ choose(2)
> a ~ 0 # 12
-> a
> b # 3
-> b
~ choose(1)
> a ~ 1 # 12
-> a
> b # 3
-> b
~ choose(1)

View file

@ -0,0 +1,132 @@
local _={}
_[67]={3}
_[66]={12}
_[65]={3}
_[64]={25}
_[63]={}
_[62]={}
_[61]={}
_[60]={}
_[59]={}
_[58]={}
_[57]={}
_[56]={text="b",tags=_[67]}
_[55]={text="a",tags=_[66]}
_[54]={}
_[53]={text="b",tags=_[65]}
_[52]={}
_[51]={text="b",tags=_[64]}
_[50]={text="a",tags=_[63]}
_[49]={}
_[48]={text="b",tags=_[62]}
_[47]={}
_[46]={text="b",tags=_[61]}
_[45]={text="a",tags=_[60]}
_[44]={}
_[43]={text="b",tags=_[59]}
_[42]={text="a",tags=_[58]}
_[41]={text="-> a",tags=_[57]}
_[40]={_[56]}
_[39]={_[55]}
_[38]={text="-> b",tags=_[54]}
_[37]={_[53]}
_[36]={text="-> b",tags=_[52]}
_[35]={_[51]}
_[34]={_[50]}
_[33]={text="-> b",tags=_[49]}
_[32]={_[48]}
_[31]={text="-> b",tags=_[47]}
_[30]={_[46]}
_[29]={_[45]}
_[28]={text="-> a",tags=_[44]}
_[27]={_[43]}
_[26]={_[42]}
_[25]={_[41]}
_[24]={_[39],_[40]}
_[23]={_[38]}
_[22]={_[37]}
_[21]={_[36]}
_[20]={_[34],_[35]}
_[19]={_[33]}
_[18]={_[32]}
_[17]={_[31]}
_[16]={_[29],_[30]}
_[15]={_[28]}
_[14]={_[26],_[27]}
_[13]={"return"}
_[12]={"text",_[25]}
_[11]={"choice",_[24]}
_[10]={"text",_[23]}
_[9]={"choice",_[22]}
_[8]={"text",_[21]}
_[7]={"choice",_[20]}
_[6]={"text",_[19]}
_[5]={"choice",_[18]}
_[4]={"text",_[17]}
_[3]={"choice",_[16]}
_[2]={"text",_[15]}
_[1]={"choice",_[14]}
return {_[1],_[2],_[3],_[4],_[5],_[6],_[7],_[8],_[9],_[10],_[11],_[12],_[13]}
--[[
{ "choice", { { {
tags = {},
text = "a"
} }, { {
tags = {},
text = "b"
} } } }
{ "text", { {
tags = {},
text = "-> a"
} } }
{ "choice", { { {
tags = {},
text = "a"
} }, { {
tags = {},
text = "b"
} } } }
{ "text", { {
tags = {},
text = "-> b"
} } }
{ "choice", { { {
tags = {},
text = "b"
} } } }
{ "text", { {
tags = {},
text = "-> b"
} } }
{ "choice", { { {
tags = {},
text = "a"
} }, { {
tags = { 25 },
text = "b"
} } } }
{ "text", { {
tags = {},
text = "-> b"
} } }
{ "choice", { { {
tags = { 3 },
text = "b"
} } } }
{ "text", { {
tags = {},
text = "-> b"
} } }
{ "choice", { { {
tags = { 12 },
text = "a"
} }, { {
tags = { 3 },
text = "b"
} } } }
{ "text", { {
tags = {},
text = "-> a"
} } }
{ "return" }
]]--

View file

@ -1,3 +1,3 @@
ko ~ 0
ok ~ 1
ok bis ~
ok bis ~ 1

View file

@ -0,0 +1,6 @@
$ f
b
a {f ~ 5} c
a {f ~ 0} c

View file

@ -0,0 +1,35 @@
local _={}
_[13]={}
_[12]={}
_[11]={}
_[10]={tags=_[13],text="c"}
_[9]={tags=_[13],text="a "}
_[8]={tags=_[11],text=" c"}
_[7]={tags=_[12],text="b"}
_[6]={tags=_[11],text="a "}
_[5]={_[9],_[10]}
_[4]={_[6],_[7],_[8]}
_[3]={"return"}
_[2]={"text",_[5]}
_[1]={"text",_[4]}
return {_[1],_[2],_[3]}
--[[
{ "text", { {
tags = <1>{},
text = "a "
}, {
tags = {},
text = "b"
}, {
tags = <table 1>,
text = " c"
} } }
{ "text", { {
tags = <1>{},
text = "a "
}, {
tags = <table 1>,
text = "c"
} } }
{ "return" }
]]--

View file

@ -0,0 +1,3 @@
a
b
{" "}c

View file

@ -0,0 +1,28 @@
local _={}
_[10]={}
_[9]={}
_[8]={}
_[7]={text="c",tags=_[10]}
_[6]={text="",tags=_[10]}
_[5]={text="b ",tags=_[9]}
_[4]={text="a ",tags=_[8]}
_[3]={_[4],_[5],_[6],_[7]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
tags = {},
text = "a "
}, {
tags = {},
text = "b "
}, {
tags = <1>{},
text = ""
}, {
tags = <table 1>,
text = "c"
} } }
{ "return" }
]]--

View file

@ -0,0 +1,5 @@
a
b
{" "}c

View file

@ -0,0 +1,34 @@
local _={}
_[14]={}
_[13]={}
_[12]={}
_[11]={tags=_[14],text="c"}
_[10]={tags=_[14],text=" "}
_[9]={tags=_[13],text="b"}
_[8]={tags=_[12],text="a"}
_[7]={_[10],_[11]}
_[6]={_[9]}
_[5]={_[8]}
_[4]={"return"}
_[3]={"text",_[7]}
_[2]={"text",_[6]}
_[1]={"text",_[5]}
return {_[1],_[2],_[3],_[4]}
--[[
{ "text", { {
tags = {},
text = "a"
} } }
{ "text", { {
tags = {},
text = "b"
} } }
{ "text", { {
tags = <1>{},
text = " "
}, {
tags = <table 1>,
text = "c"
} } }
{ "return" }
]]--

View file

@ -1,7 +1,8 @@
$ f
# "a":"a"
a
~ 1 # "b":"b"
~ 1 # "x":"x"
# "b":"b"
§ p
b # "c":"c"

View file

@ -2,6 +2,6 @@ expression {"{"a"}"}
quote {"\""}
other codes {"\n"} {"\\"} {"\t"}
other codes {"\n"} {"\\"} {"\t"} \{braces}
{"escaping expressions {"a"+"bc"} and stuff \\ and quotes \""}
{"escaping expressions {"a"+"bc"} and \{stuff} \\ and quotes \""}

View file

@ -1,21 +1,22 @@
local _={}
_[25]={}
_[24]={}
_[23]={}
_[22]={}
_[21]={}
_[20]={tags=_[24],text="escaping expressions abc and stuff \\ and quotes \""}
_[19]={tags=_[23],text="\9"}
_[18]={tags=_[23],text=" "}
_[17]={tags=_[23],text="\\"}
_[16]={tags=_[23],text=" "}
_[15]={tags=_[23],text="\n"}
_[14]={tags=_[23],text="other codes "}
_[13]={tags=_[22],text="\""}
_[12]={tags=_[22],text="quote "}
_[11]={tags=_[21],text="a"}
_[10]={tags=_[21],text="expression "}
_[9]={_[20]}
_[8]={_[14],_[15],_[16],_[17],_[18],_[19]}
_[21]={text="escaping expressions abc and {stuff} \\ and quotes \"",tags=_[25]}
_[20]={text=" {braces}",tags=_[24]}
_[19]={text="\9",tags=_[24]}
_[18]={text=" ",tags=_[24]}
_[17]={text="\\",tags=_[24]}
_[16]={text=" ",tags=_[24]}
_[15]={text="\n",tags=_[24]}
_[14]={text="other codes ",tags=_[24]}
_[13]={text="\"",tags=_[23]}
_[12]={text="quote ",tags=_[23]}
_[11]={text="a",tags=_[22]}
_[10]={text="expression ",tags=_[22]}
_[9]={_[21]}
_[8]={_[14],_[15],_[16],_[17],_[18],_[19],_[20]}
_[7]={_[12],_[13]}
_[6]={_[10],_[11]}
_[5]={"return"}
@ -57,10 +58,13 @@ return {_[1],_[2],_[3],_[4],_[5]}
}, {
tags = <table 1>,
text = "\t"
}, {
tags = <table 1>,
text = " {braces}"
} } }
{ "text", { {
tags = {},
text = 'escaping expressions abc and stuff \\ and quotes "'
text = 'escaping expressions abc and {stuff} \\ and quotes "'
} } }
{ "return" }
]]--

12
test/tests/subtext.ans Normal file
View file

@ -0,0 +1,12 @@
$ button
A # 2:2
Press
A # 5
to jump.
Press [A#5] to jump.
Press [{button}#1] to jump.
Press [-[button#3:3]-#1] to jump.

86
test/tests/subtext.lua Normal file
View file

@ -0,0 +1,86 @@
local _={}
_[33]={1,[3]=3}
_[32]={1}
_[31]={}
_[30]={1,2}
_[29]={}
_[28]={5}
_[27]={}
_[26]={}
_[25]={5}
_[24]={}
_[23]={tags=_[31],text=" to jump."}
_[22]={tags=_[32],text="-"}
_[21]={tags=_[33],text="button"}
_[20]={tags=_[32],text="-"}
_[19]={tags=_[31],text="Press "}
_[18]={tags=_[29],text="to jump."}
_[17]={tags=_[30],text="A "}
_[16]={tags=_[29],text="Press "}
_[15]={tags=_[27],text=" to jump."}
_[14]={tags=_[28],text="A"}
_[13]={tags=_[27],text="Press "}
_[12]={tags=_[26],text="to jump."}
_[11]={tags=_[25],text="A "}
_[10]={tags=_[24],text="Press "}
_[9]={_[19],_[20],_[21],_[22],_[23]}
_[8]={_[16],_[17],_[18]}
_[7]={_[13],_[14],_[15]}
_[6]={_[10],_[11],_[12]}
_[5]={"return"}
_[4]={"text",_[9]}
_[3]={"text",_[8]}
_[2]={"text",_[7]}
_[1]={"text",_[6]}
return {_[1],_[2],_[3],_[4],_[5]}
--[[
{ "text", { {
tags = {},
text = "Press "
}, {
tags = { 5 },
text = "A "
}, {
tags = {},
text = "to jump."
} } }
{ "text", { {
tags = <1>{},
text = "Press "
}, {
tags = { 5 },
text = "A"
}, {
tags = <table 1>,
text = " to jump."
} } }
{ "text", { {
tags = <1>{},
text = "Press "
}, {
tags = { 1, 2 },
text = "A "
}, {
tags = <table 1>,
text = "to jump."
} } }
{ "text", { {
tags = <1>{},
text = "Press "
}, {
tags = <2>{ 1 },
text = "-"
}, {
tags = { 1,
[3] = 3
},
text = "button"
}, {
tags = <table 2>,
text = "-"
}, {
tags = <table 1>,
text = " to jump."
} } }
{ "return" }
]]--

View file

@ -1,4 +0,0 @@
# 1
foo
~ 1 #
bar

View file

@ -1,19 +0,0 @@
local _={}
_[7]={1}
_[6]={1}
_[5]={tags=_[7],text="bar"}
_[4]={tags=_[6],text="foo"}
_[3]={_[4],_[5]}
_[2]={"return"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
tags = { 1 },
text = "foo"
}, {
tags = { 1 },
text = "bar"
} } }
{ "return" }
]]--

View file

@ -1,4 +1,4 @@
# 1
foo
~ 1 # "a": [2,3]
bar
~ 1 # "b": [1,2]
bar ~ 1 # "a": [2,3]

View file

@ -0,0 +1,7 @@
$ f
b
a {f # 5} c
# 2:2
a {f # 5} c

View file

@ -0,0 +1,42 @@
local _={}
_[15]={5,2}
_[14]={[2]=2}
_[13]={5}
_[12]={}
_[11]={tags=_[14],text=" c"}
_[10]={tags=_[15],text="b"}
_[9]={tags=_[14],text="a "}
_[8]={tags=_[12],text=" c"}
_[7]={tags=_[13],text="b"}
_[6]={tags=_[12],text="a "}
_[5]={_[9],_[10],_[11]}
_[4]={_[6],_[7],_[8]}
_[3]={"return"}
_[2]={"text",_[5]}
_[1]={"text",_[4]}
return {_[1],_[2],_[3]}
--[[
{ "text", { {
tags = <1>{},
text = "a "
}, {
tags = { 5 },
text = "b"
}, {
tags = <table 1>,
text = " c"
} } }
{ "text", { {
tags = <1>{
[2] = 2
},
text = "a "
}, {
tags = { 5, 2 },
text = "b"
}, {
tags = <table 1>,
text = " c"
} } }
{ "return" }
]]--

View file

@ -0,0 +1,7 @@
expression \{a}
quote \"
other codes \n \\ \t
decorators \# tag \~ condition \$ fn

View file

@ -0,0 +1,38 @@
local _={}
_[17]={}
_[16]={}
_[15]={}
_[14]={}
_[13]={tags=_[17],text="decorators # tag ~ condition $ fn"}
_[12]={tags=_[16],text="other codes \n \\ \9"}
_[11]={tags=_[15],text="quote \""}
_[10]={tags=_[14],text="expression {a}"}
_[9]={_[13]}
_[8]={_[12]}
_[7]={_[11]}
_[6]={_[10]}
_[5]={"return"}
_[4]={"text",_[9]}
_[3]={"text",_[8]}
_[2]={"text",_[7]}
_[1]={"text",_[6]}
return {_[1],_[2],_[3],_[4],_[5]}
--[[
{ "text", { {
tags = {},
text = "expression {a}"
} } }
{ "text", { {
tags = {},
text = 'quote "'
} } }
{ "text", { {
tags = {},
text = "other codes \n \\ \t"
} } }
{ "text", { {
tags = {},
text = "decorators # tag ~ condition $ fn"
} } }
{ "return" }
]]--