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

Add variable constraints, rename type annotation checks to constraints, rename custom type to annotation

This commit is contained in:
Étienne Fildadut 2022-09-08 20:43:36 +09:00
parent 92a496e584
commit 3e658e4780
16 changed files with 237 additions and 118 deletions

View file

@ -139,7 +139,7 @@ $ fn
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. 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.
A parameter list can be optionally given after the identifier. Parameter names are identifiers, with eventually an alias (after a `:`) and a default value (after a `=`), and then a type annotation (after a `::`). It is enclosed with paranthesis and contain a comma-separated list of identifiers: A parameter list can be optionally given after the identifier. Parameter names are identifiers, with eventually an alias (after a `:`) and a default value (after a `=`), and then a type constraint (after a `::`). It is enclosed with paranthesis and contain a comma-separated list of identifiers:
``` ```
$ f(a, b: alias for b, c="default for c", d: alias for d = "default for d") $ f(a, b: alias for b, c="default for c", d: alias for d = "default for d")
@ -195,7 +195,7 @@ $ g()
This is basically the behaviour you'd expect from functions in most other programming languages, and what you would use in Anselme any time you don't care about storing the function variables or want the exact same initial function variables each time you call the function (e.g. recursion). Scoped variables are not kept in save files, and are not affected by checkpointing. This is basically the behaviour you'd expect from functions in most other programming languages, and what you would use in Anselme any time you don't care about storing the function variables or want the exact same initial function variables each time you call the function (e.g. recursion). Scoped variables are not kept in save files, and are not affected by checkpointing.
Functions with the same name can be defined, as long as they have a different arguments. Functions will be selected based on the number of arguments given, their name and their type annotation: Functions with the same name can be defined, as long as they have a different arguments. Functions will be selected based on the number of arguments given, their name and their type constraint:
``` ```
$ f(a, b) $ f(a, b)
@ -289,7 +289,7 @@ Is 3: {object.a}
Is 1: {class.a} Is 1: {class.a}
``` ```
Note that the new object returned by the class is also automatically given a type that is a reference to the class. This can be used to define methods/function that operate only on objects based on this specific class. Note that the new object returned by the class is also automatically given an annotation that is a reference to the class. This can be used to define methods/function that operate only on objects based on this specific class.
``` ```
% class % class
@ -620,7 +620,7 @@ Default types are:
* `pair`: a couple of values. Types can be mixed. Can be defined using equal sign `"key"=5`. Pairs named by a string that is also a valid identifier can be created using the `key=5` shorthand syntax; `key` will not be interpreted as the variable `key` but the string `"key"` (if `key` is a variable and you want to force the use of its value as a key instead of the string `"key"`, you can wrap it in parentheses). * `pair`: a couple of values. Types can be mixed. Can be defined using equal sign `"key"=5`. Pairs named by a string that is also a valid identifier can be created using the `key=5` shorthand syntax; `key` will not be interpreted as the variable `key` but the string `"key"` (if `key` is a variable and you want to force the use of its value as a key instead of the string `"key"`, you can wrap it in parentheses).
* `type`: a couple of values. Types can be mixed. Can be defined using colon `expr::type`. The second value is used in type checks, this is intended to be use to give a custom type to a value. * `annotated`: a couple of values. Types can be mixed. Can be defined using colon `expr::type`. The second value is used in type constraints, this is intended to be use to give a custom type to a value.
* `function reference`: reference to one or more function(s) with a given name. Can be defined using `&function name`, which will create a reference to every function with this name accessible from the current namespace. Can be called as if it was the original function using `func ref!` and `func ref(args)`. * `function reference`: reference to one or more function(s) with a given name. Can be defined using `&function name`, which will create a reference to every function with this name accessible from the current namespace. Can be called as if it was the original function using `func ref!` and `func ref(args)`.
@ -797,7 +797,7 @@ $ f(a, b...)
[2,3,4,5] [2,3,4,5]
``` ```
Anselme use dynamic dispatch, meaning the correct function is selected at runtime. The correct function is selected based on number of arguments, argument names, and argument type annotations. The function with the most specific arguments will be selected. If several functions match, an error is thrown. Anselme use dynamic dispatch, meaning the correct function is selected at runtime. The correct function is selected based on number of arguments, argument names, and argument type constraint. The function with the most specific arguments will be selected. If several functions match, an error is thrown.
``` ```
$ fn(x::number, y) $ fn(x::number, y)
@ -827,6 +827,8 @@ $ g(x, a="t")
error, can't select unique function: {g(5)} error, can't select unique function: {g(5)}
``` ```
Note that types constraints are expected to be constant and are evaluated only once. Default values, however, are evaluated each time the function is called (and the user didn't explicitely give an argument that would replace this default).
#### Checkpoint calls #### Checkpoint calls
Most of the time, you should'nt need to call checkpoints yourself - they will be automatically be set as the active checkpoint when the interperter reach their line, and they will be automatically called when resuming its parent function. Most of the time, you should'nt need to call checkpoints yourself - they will be automatically be set as the active checkpoint when the interperter reach their line, and they will be automatically called when resuming its parent function.
@ -980,7 +982,7 @@ This only works on strings:
`a = b`: evaluate a and b, returns a new pair with a as key and b as value. If a is an identifier, will interpret it as a string (and not a variable; you can wrap a in parentheses if you want to use the value associated with variable a instead). `a = b`: evaluate a and b, returns a new pair with a as key and b as value. If a is an identifier, will interpret it as a string (and not a variable; you can wrap a in parentheses if you want to use the value associated with variable a instead).
`a :: b`: evaluate a and b, returns a new typed value with a as value and b as type. `a :: b`: evaluate a and b, returns a new annotated value with a as value and b as the annotation. This annotation will be checked in type constraints.
`a # b`: evaluates b, then evaluates a whith b added to the active tags. Returns a. `a # b`: evaluates b, then evaluates a whith b added to the active tags. Returns a.
@ -1030,10 +1032,14 @@ This only works on strings:
`error(str)`: throw an error with the specified message `error(str)`: throw an error with the specified message
`raw(v)`: return v, stripped of its custom types `annotation(v::annotated)`: returns v's annotation
`unannotated(v)`: return v, eventual annotations removed
`type(v)`: return v's type `type(v)`: return v's type
`is a(v, type or annotation)`: check if v is of a certain type or annotation
#### Built-in variables #### Built-in variables
Variables for default types (each is associated to a string of the internal variable type name): `nil`, `number`, `string`, `list`, `pair`, `function reference`, `variable reference`. Variables for default types (each is associated to a string of the internal variable type name): `nil`, `number`, `string`, `list`, `pair`, `function reference`, `variable reference`.

View file

@ -667,6 +667,7 @@ local vm_mt = {
builtin_aliases = self.state.builtin_aliases, builtin_aliases = self.state.builtin_aliases,
aliases = setmetatable({}, { __index = self.state.aliases }), aliases = setmetatable({}, { __index = self.state.aliases }),
functions = self.state.functions, -- no need for a cache as we can't define or modify any function from the interpreter for now functions = self.state.functions, -- no need for a cache as we can't define or modify any function from the interpreter for now
variable_constraints = self.state.variable_constraints, -- no cache as constraints are expected to be constant
variables = setmetatable({}, { variables = setmetatable({}, {
__index = function(variables, k) __index = function(variables, k)
local cache = getmetatable(variables).cache local cache = getmetatable(variables).cache
@ -759,6 +760,9 @@ return setmetatable(anselme, {
-- }, ... -- }, ...
-- }, ... -- }, ...
}, },
variable_constraints = {
-- foo = { constraint }, ...
},
variables = { variables = {
-- foo = { -- foo = {
-- type = "number", -- type = "number",

View file

@ -96,6 +96,27 @@ common = {
common.scope:set_last_scope(state, fn) common.scope:set_last_scope(state, fn)
end end
end, end,
--- checks if the value is compatible with the variable's (eventual) constraint
-- returns depth, or math.huge if no constraint
-- returns nil, err
check_constraint = function(state, fqm, val)
local constraint = state.variable_constraints[fqm]
if constraint then
if not constraint.value then
local v, e = eval(state, constraint.pending)
if not v then
return nil, ("%s; while evaluating constraint for variable %q"):format(e, fqm)
end
constraint.value = v
end
local depth = common.is_of_type(val, constraint.value)
if not depth then
return nil, ("constraint check failed")
end
return depth
end
return math.huge
end,
--- returns a variable's value, evaluating a pending expression if neccessary --- returns a variable's value, evaluating a pending expression if neccessary
-- if you're sure the variable has already been evaluated, use state.variables[fqm] directly -- if you're sure the variable has already been evaluated, use state.variables[fqm] directly
-- return var -- return var
@ -107,15 +128,25 @@ common = {
if not v then if not v then
return nil, ("%s; while evaluating default value for variable %q defined at %s"):format(e, fqm, var.value.source) return nil, ("%s; while evaluating default value for variable %q defined at %s"):format(e, fqm, var.value.source)
end end
state.variables[fqm] = v local s, err = common.set_variable(state, fqm, v)
if not s then return nil, err end
return v return v
else else
return var return var
end end
end, end,
--- set the value of a variable --- set the value of a variable
-- returns true
-- returns nil, err
set_variable = function(state, name, val) set_variable = function(state, name, val)
if val.type ~= "pending definition" then
local s, e = common.check_constraint(state, name, val)
if not s then
return nil, ("%s; while assigning value to variable %q"):format(e, name)
end
end
state.variables[name] = val state.variables[name] = val
return true
end, end,
--- handle scoped function --- handle scoped function
scope = { scope = {
@ -207,7 +238,7 @@ common = {
if a.type ~= b.type then if a.type ~= b.type then
return false return false
end end
if a.type == "pair" or a.type == "type" then if a.type == "pair" or a.type == "annotated" then
return common.compare(a.value[1], b.value[1]) and common.compare(a.value[2], b.value[2]) return common.compare(a.value[1], b.value[1]) and common.compare(a.value[2], b.value[2])
elseif a.type == "list" then elseif a.type == "list" then
if #a.value ~= #b.value then if #a.value ~= #b.value then
@ -300,18 +331,23 @@ common = {
end end
return true return true
end, end,
--- check if an anselme value is of a certain type --- check if an anselme value is of a certain type or annotation
-- specificity(number): if var is of type type. lower is more specific -- specificity(number): if var is of type type. lower is more specific
-- false: if not -- false: if not
is_of_type = function(var, type) is_of_type = function(var, type)
local depth = 1 local depth = 1
-- var has a custom type -- var has a custom annotation
if var.type == "type" then if var.type == "annotated" then
-- special case: if we just want to see if a value is annotated
if type.type == "string" and type.value == "annotated" then
return depth
end
-- check annotation
local var_type = var.value[2] local var_type = var.value[2]
while true do while true do
if common.compare(var_type, type) then -- same type if common.compare(var_type, type) then -- same type
return depth return depth
elseif var_type.type == "type" then -- compare parent type elseif var_type.type == "annotated" then -- compare parent type
depth = depth + 1 depth = depth + 1
var_type = var_type.value[2] var_type = var_type.value[2]
else -- no parent, fall back on base type else -- no parent, fall back on base type
@ -326,7 +362,7 @@ common = {
end, end,
-- return a pretty printable type value for var -- return a pretty printable type value for var
pretty_type = function(var) pretty_type = function(var)
if var.type == "type" then if var.type == "annotated" then
return common.format(var.value[2]) return common.format(var.value[2])
else else
return var.type return var.type

View file

@ -1,5 +1,5 @@
local expression local expression
local to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable, tags, eval_text_callback, events, flatten_list, set_variable, scope local to_lua, from_lua, eval_text, truthy, format, pretty_type, get_variable, tags, eval_text_callback, events, flatten_list, set_variable, scope, check_constraint
local run local run
@ -71,7 +71,8 @@ local function eval(state, exp)
local name = exp.left.name local name = exp.left.name
local val, vale = eval(state, exp.right) local val, vale = eval(state, exp.right)
if not val then return nil, vale end if not val then return nil, vale end
set_variable(state, name, val) local s, e = set_variable(state, name, val)
if not s then return nil, e end
return val return val
else else
return nil, ("don't know how to perform assignment on %s expression"):format(exp.left.type) return nil, ("don't know how to perform assignment on %s expression"):format(exp.left.type)
@ -240,24 +241,19 @@ local function eval(state, exp)
used_args[j] = true used_args[j] = true
end end
if val then if val then
-- check type annotation -- check type constraint
if param.type_annotation then local depth, err = check_constraint(state, param.full_name, val)
local v, e = eval(state, param.type_annotation)
if not v then return nil, e end
local depth = is_of_type(val, v)
if not depth then if not depth then
ok = false ok = false
local v = state.variable_constraints[param.full_name].value
table.insert(tried_function_error_messages, ("%s: argument %s is not of expected type %s"):format(fn.pretty_signature, param.name, format(v) or v)) table.insert(tried_function_error_messages, ("%s: argument %s is not of expected type %s"):format(fn.pretty_signature, param.name, format(v) or v))
break break
end end
depths[j] = depth depths[j] = depth
else
depths[j] = math.huge
end
-- set -- set
variant_args[param.full_name] = val variant_args[param.full_name] = val
-- default: evaluate once function is selected -- default: evaluate once function is selected
-- there's no need to type check because the type annotation is already the default value's type, because of syntax -- there's no need to type check because the type constraint is already the default value's type, because of syntax
elseif param.default then elseif param.default then
variant_args[param.full_name] = { type = "pending definition", value = { expression = param.default, source = fn.source } } variant_args[param.full_name] = { type = "pending definition", value = { expression = param.default, source = fn.source } }
else else
@ -282,21 +278,15 @@ local function eval(state, exp)
end end
-- assignment arg -- assignment arg
if ok and exp.assignment then if ok and exp.assignment then
-- check type annotation -- check type constraint
local param = fn.assignment local param = fn.assignment
if param.type_annotation then local depth, err = check_constraint(state, param.full_name, assignment)
local v, e = eval(state, param.type_annotation)
if not v then return nil, e end
local depth = is_of_type(assignment, v)
if not depth then if not depth then
ok = false ok = false
local v = state.variable_constraints[param.full_name].value
table.insert(tried_function_error_messages, ("%s: argument %s is not of expected type %s"):format(fn.pretty_signature, param.name, format(v) or v)) table.insert(tried_function_error_messages, ("%s: argument %s is not of expected type %s"):format(fn.pretty_signature, param.name, format(v) or v))
else end
depths.assignment = depth depths.assignment = depth
end
else
depths.assignment = math.huge
end
-- set -- set
variant_args[param.full_name] = assignment variant_args[param.full_name] = assignment
end end
@ -360,7 +350,8 @@ local function eval(state, exp)
end end
-- set arguments -- set arguments
for name, val in pairs(selected_variant.args_to_set) do for name, val in pairs(selected_variant.args_to_set) do
set_variable(state, name, val) local s, e = set_variable(state, name, val)
if not s then return nil, e end
end end
-- get function vars -- get function vars
local checkpoint, checkpointe = get_variable(state, fn.namespace.."🔖") local checkpoint, checkpointe = get_variable(state, fn.namespace.."🔖")
@ -393,11 +384,11 @@ local function eval(state, exp)
else else
return nil, ("%s; in Lua function %q"):format(e or "raw function returned nil and no error message", exp.called_name) return nil, ("%s; in Lua function %q"):format(e or "raw function returned nil and no error message", exp.called_name)
end end
-- untyped raw mode: same as raw, but strips custom types from the arguments -- unannotated raw mode: same as raw, but strips custom annotations from the arguments
elseif lua_fn.mode == "untyped raw" then elseif lua_fn.mode == "unannotated raw" then
-- extract value from custom types -- extract value from custom types
for i, arg in ipairs(final_args) do for i, arg in ipairs(final_args) do
if arg.type == "type" then if arg.type == "annotated" then
final_args[i] = arg.value[1] final_args[i] = arg.value[1]
end end
end end
@ -405,7 +396,7 @@ local function eval(state, exp)
if r then if r then
ret = r ret = r
else else
return nil, ("%s; in Lua function %q"):format(e or "untyped raw function returned nil and no error message", exp.called_name) return nil, ("%s; in Lua function %q"):format(e or "unannotated raw function returned nil and no error message", exp.called_name)
end end
-- normal mode: convert args to Lua and convert back Lua value to Anselme -- normal mode: convert args to Lua and convert back Lua value to Anselme
elseif lua_fn.mode == nil then elseif lua_fn.mode == nil then
@ -444,14 +435,15 @@ local function eval(state, exp)
if not ret then return nil, e end if not ret then return nil, e end
end end
-- update function vars -- update function vars
set_variable(state, fn.namespace.."👁️", { local s, e = set_variable(state, fn.namespace.."👁️", {
type = "number", type = "number",
value = seen.value + 1 value = seen.value + 1
}) })
if not s then return nil, e end
-- for classes: build resulting object -- for classes: build resulting object
if fn.subtype == "class" then if fn.subtype == "class" then
local object = { local object = {
type = "type", type = "annotated",
value = { value = {
{ {
type = "object", type = "object",
@ -519,6 +511,6 @@ run = require((...):gsub("expression$", "interpreter")).run
expression = require((...):gsub("interpreter%.expression$", "parser.expression")) expression = require((...):gsub("interpreter%.expression$", "parser.expression"))
flatten_list = require((...):gsub("interpreter%.expression$", "parser.common")).flatten_list flatten_list = require((...):gsub("interpreter%.expression$", "parser.common")).flatten_list
local common = require((...):gsub("expression$", "common")) local common = require((...):gsub("expression$", "common"))
to_lua, from_lua, eval_text, is_of_type, truthy, format, pretty_type, get_variable, tags, eval_text_callback, events, set_variable, scope = 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, common.set_variable, common.scope to_lua, from_lua, eval_text, truthy, format, pretty_type, get_variable, tags, eval_text_callback, events, set_variable, scope, check_constraint = common.to_lua, common.from_lua, common.eval_text, common.truthy, common.format, common.pretty_type, common.get_variable, common.tags, common.eval_text_callback, common.events, common.set_variable, common.scope, common.check_constraint
return eval return eval

View file

@ -114,14 +114,16 @@ run_line = function(state, line)
elseif line.type == "function" and line.subtype == "checkpoint" then elseif line.type == "function" and line.subtype == "checkpoint" then
local reached, reachede = get_variable(state, line.namespace.."🏁") local reached, reachede = get_variable(state, line.namespace.."🏁")
if not reached then return nil, reachede end if not reached then return nil, reachede end
set_variable(state, line.namespace.."🏁", { local s, e = set_variable(state, line.namespace.."🏁", {
type = "number", type = "number",
value = reached.value + 1 value = reached.value + 1
}) })
set_variable(state, line.parent_function.namespace.."🔖", { if not s then return nil, e end
s, e = set_variable(state, line.parent_function.namespace.."🔖", {
type = "function reference", type = "function reference",
value = { line.name } value = { line.name }
}) })
if not s then return nil, e end
merge_state(state) merge_state(state)
else else
return nil, ("unknown line type %q; at %s"):format(line.type, line.source) return nil, ("unknown line type %q; at %s"):format(line.type, line.source)
@ -160,21 +162,24 @@ run_block = function(state, block, resume_from_there, i, j)
if not seen then return nil, seene end if not seen then return nil, seene end
local checkpoint, checkpointe = get_variable(state, parent_line.parent_function.namespace.."🔖") local checkpoint, checkpointe = get_variable(state, parent_line.parent_function.namespace.."🔖")
if not checkpoint then return nil, checkpointe end if not checkpoint then return nil, checkpointe end
set_variable(state, parent_line.namespace.."👁️", { local s, e = set_variable(state, parent_line.namespace.."👁️", {
type = "number", type = "number",
value = seen.value + 1 value = seen.value + 1
}) })
set_variable(state, parent_line.namespace.."🏁", { if not s then return nil, e end
s, e = set_variable(state, parent_line.namespace.."🏁", {
type = "number", type = "number",
value = reached.value + 1 value = reached.value + 1
}) })
if not s then return nil, e end
-- don't update checkpoint if an already more precise checkpoint is set -- don't update checkpoint if an already more precise checkpoint is set
-- (since we will go up the whole checkpoint hierarchy when resuming from a nested checkpoint) -- (since we will go up the whole checkpoint hierarchy when resuming from a nested checkpoint)
if checkpoint.type == "nil" or not checkpoint.value[1]:match("^"..escape(parent_line.name)) then if checkpoint.type == "nil" or not checkpoint.value[1]:match("^"..escape(parent_line.name)) then
set_variable(state, parent_line.parent_function.namespace.."🔖", { s, e = set_variable(state, parent_line.parent_function.namespace.."🔖", {
type = "function reference", type = "function reference",
value = { parent_line.name } value = { parent_line.name }
}) })
if not s then return nil, e end
end end
merge_state(state) merge_state(state)
end end

View file

@ -307,8 +307,8 @@ common = {
if p.alias then if p.alias then
sig = sig .. ":" .. p.alias sig = sig .. ":" .. p.alias
end end
if p.type_annotation then if p.type_constraint then
sig = sig .. "::" .. p.type_annotation sig = sig .. "::" .. p.type_constraint
end end
if p.default then if p.default then
sig = sig .. "=" .. p.default sig = sig .. "=" .. p.default

View file

@ -8,17 +8,17 @@ local function parse(state)
for i=#state.queued_lines, 1, -1 do for i=#state.queued_lines, 1, -1 do
local l = state.queued_lines[i] local l = state.queued_lines[i]
local line, namespace = l.line, l.namespace local line, namespace = l.line, l.namespace
-- default arguments and type annotation -- default arguments and type constraints
if line.type == "function" then if line.type == "function" then
for _, param in ipairs(line.params) do for _, param in ipairs(line.params) do
-- get type annotation -- get type constraints
if param.type_annotation then if param.type_constraint then
local type_exp, rem = expression(param.type_annotation, state, namespace) local type_exp, rem = expression(param.type_constraint, state, namespace)
if not type_exp then return nil, ("in type annotation, %s; at %s"):format(rem, line.source) end if not type_exp then return nil, ("in type constraint, %s; at %s"):format(rem, line.source) end
if rem:match("[^%s]") then if rem:match("[^%s]") then
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param.full_name, rem, line.source) return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param.full_name, rem, line.source)
end end
param.type_annotation = type_exp state.variable_constraints[param.full_name] = { pending = type_exp }
end end
-- get default value -- get default value
if param.default then if param.default then
@ -28,20 +28,20 @@ local function parse(state)
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param.full_name, rem, line.source) return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param.full_name, rem, line.source)
end end
param.default = default_exp param.default = default_exp
-- extract type annotation from default value -- extract type constraint from default value
if default_exp.type == "function call" and default_exp.called_name == "_::_" then if default_exp.type == "function call" and default_exp.called_name == "_::_" then
param.type_annotation = default_exp.argument.expression.right state.variable_constraints[param.full_name] = { pending = default_exp.argument.expression.right }
end end
end end
end end
-- assignment argument -- assignment argument
if line.assignment and line.assignment.type_annotation then if line.assignment and line.assignment.type_constraint then
local type_exp, rem = expression(line.assignment.type_annotation, state, namespace) local type_exp, rem = expression(line.assignment.type_constraint, state, namespace)
if not type_exp then return nil, ("in type annotation, %s; at %s"):format(rem, line.source) end if not type_exp then return nil, ("in type constraint, %s; at %s"):format(rem, line.source) end
if rem:match("[^%s]") then if rem:match("[^%s]") then
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(line.assignment.full_name, rem, line.source) return nil, ("unexpected characters after parameter %q: %q; at %s"):format(line.assignment.full_name, rem, line.source)
end end
line.assignment.type_annotation = type_exp state.variable_constraints[line.assignment.full_name] = { pending = type_exp }
end end
-- get list of scoped variables -- get list of scoped variables
-- (note includes every variables in the namespace of subnamespace, so subfunctions are scoped alongside this function) -- (note includes every variables in the namespace of subnamespace, so subfunctions are scoped alongside this function)
@ -63,6 +63,15 @@ local function parse(state)
-- variable pending definition: expression will be evaluated when variable is needed -- variable pending definition: expression will be evaluated when variable is needed
if line.type == "definition" then if line.type == "definition" then
state.variables[line.fqm].value.expression = line.expression state.variables[line.fqm].value.expression = line.expression
-- parse constraints
if line.constraint then
local type_exp, rem2 = expression(line.constraint, state, namespace)
if not type_exp then return nil, ("in type constraint, %s; at %s"):format(rem2, line.source) end
if rem2:match("[^%s]") then
return nil, ("unexpected characters after variable %q: %q; at %s"):format(line.fqm, rem2, line.source)
end
state.variable_constraints[line.fqm] = { pending = type_exp }
end
end end
end end
-- text (text & choice lines) -- text (text & choice lines)

View file

@ -134,17 +134,17 @@ local function parse_line(line, state, namespace, parent_function)
local ok_param_alias, param_alias local ok_param_alias, param_alias
ok_param_alias, param_rem, param_alias = maybe_alias(param_rem, param_fqm, func_namespace, line, state) ok_param_alias, param_rem, param_alias = maybe_alias(param_rem, param_fqm, func_namespace, line, state)
if not ok_param_alias then return ok_param_alias, param_rem end if not ok_param_alias then return ok_param_alias, param_rem end
-- get potential type annotation and default value -- get potential type constraints and default value
local type_annotation, default local type_constraint, default
if param_rem:match("^::") then if param_rem:match("^::") then
type_annotation = param_rem:match("^::(.*)$") type_constraint = param_rem:match("^::(.*)$")
elseif param_rem:match("^=") then elseif param_rem:match("^=") then
default = param_rem:match("^=(.*)$") default = param_rem:match("^=(.*)$")
elseif param_rem:match("[^%s]") then elseif param_rem:match("[^%s]") then
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param_fqm, param_rem, line.source) return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param_fqm, param_rem, line.source)
end end
-- add parameter -- add parameter
table.insert(r.params, { name = param_identifier, alias = param_alias, full_name = param_fqm, type_annotation = type_annotation, default = default, vararg = nil }) table.insert(r.params, { name = param_identifier, alias = param_alias, full_name = param_fqm, type_constraint = type_constraint, default = default, vararg = nil })
end end
end end
-- get assignment param -- get assignment param
@ -160,15 +160,15 @@ local function parse_line(line, state, namespace, parent_function)
local ok_param_alias, param_alias local ok_param_alias, param_alias
ok_param_alias, param_rem, param_alias = maybe_alias(param_rem, param_fqm, func_namespace, line, state) ok_param_alias, param_rem, param_alias = maybe_alias(param_rem, param_fqm, func_namespace, line, state)
if not ok_param_alias then return ok_param_alias, param_rem end if not ok_param_alias then return ok_param_alias, param_rem end
-- get potential type annotation -- get potential type constraint
local type_annotation local type_constraint
if param_rem:match("^::") then if param_rem:match("^::") then
type_annotation = param_rem:match("^::(.*)$") type_constraint = param_rem:match("^::(.*)$")
elseif param_rem:match("[^%s]") then elseif param_rem:match("[^%s]") then
return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param_fqm, param_rem, line.source) return nil, ("unexpected characters after parameter %q: %q; at %s"):format(param_fqm, param_rem, line.source)
end end
-- add parameter -- add parameter
r.assignment = { name = param_identifier, alias = param_alias, full_name = param_fqm, type_annotation = type_annotation, default = nil, vararg = nil } r.assignment = { name = param_identifier, alias = param_alias, full_name = param_fqm, type_constraint = type_constraint, default = nil, vararg = nil }
elseif rem:match("[^%s]") then elseif rem:match("[^%s]") then
return nil, ("expected end-of-line at end of function definition line, but got %q; at %s"):format(rem, line.source) return nil, ("expected end-of-line at end of function definition line, but got %q; at %s"):format(rem, line.source)
end end
@ -300,6 +300,10 @@ local function parse_line(line, state, namespace, parent_function)
local ok_alias local ok_alias
ok_alias, rem = maybe_alias(rem, fqm, namespace, line, state) ok_alias, rem = maybe_alias(rem, fqm, namespace, line, state)
if not ok_alias then return ok_alias, rem end if not ok_alias then return ok_alias, rem end
-- type constraint
if rem:match("^::(.-)=") then
r.constraint, rem = rem:match("^::%s*(.-)%s*(=.*)$")
end
-- get expression -- get expression
local exp = rem:match("^=(.*)$") local exp = rem:match("^=(.*)$")
if not exp then return nil, ("expected \"= expression\" after %q in definition line; at %s"):format(rem, line.source) end if not exp then return nil, ("expected \"= expression\" after %q in definition line; at %s"):format(rem, line.source) end
@ -509,6 +513,7 @@ local function parse(state, s, name, source)
local state_proxy = { local state_proxy = {
inject = {}, inject = {},
aliases = setmetatable({}, { __index = state.aliases }), aliases = setmetatable({}, { __index = state.aliases }),
variable_constraints = setmetatable({}, { __index = state.variable_constraints }),
variables = setmetatable({}, { __index = state.aliases }), variables = setmetatable({}, { __index = state.aliases }),
functions = setmetatable({}, { functions = setmetatable({}, {
__index = function(self, key) __index = function(self, key)
@ -541,6 +546,9 @@ local function parse(state, s, name, source)
for k,v in pairs(state_proxy.aliases) do for k,v in pairs(state_proxy.aliases) do
state.aliases[k] = v state.aliases[k] = v
end end
for k,v in pairs(state_proxy.variable_constraints) do
state.variable_constraints[k] = v
end
for k,v in pairs(state_proxy.variables) do for k,v in pairs(state_proxy.variables) do
state.variables[k] = v state.variables[k] = v
end end

View file

@ -9,6 +9,7 @@ return [[
:function reference="function reference" :function reference="function reference"
:variable reference="variable reference" :variable reference="variable reference"
:object="object" :object="object"
:annotated="annotated"
:pi=3.1415926535898 :pi=3.1415926535898
]] ]]

View file

@ -64,19 +64,19 @@ lua_functions = {
} }
end end
}, },
-- type -- annotate
["_::_(a, b)"] = { ["_::_(a, b)"] = {
mode = "raw", mode = "raw",
value = function(a, b) value = function(a, b)
return { return {
type = "type", type = "annotated",
value = { a, b } value = { a, b }
} }
end end
}, },
-- namespace -- namespace
["_._(r::function reference, name::string)"] = { ["_._(r::function reference, name::string)"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(r, n) value = function(r, n)
local state = anselme.running.state local state = anselme.running.state
local rval = r.value local rval = r.value
@ -100,7 +100,7 @@ lua_functions = {
end end
}, },
["_._(r::function reference, name::string) := v"] = { ["_._(r::function reference, name::string) := v"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(r, n, v) value = function(r, n, v)
local state = anselme.running.state local state = anselme.running.state
local rval = r.value local rval = r.value
@ -108,7 +108,8 @@ lua_functions = {
for _, ffqm in ipairs(rval) do for _, ffqm in ipairs(rval) do
local var, vfqm = find(state.aliases, state.interpreter.global_state.variables, "", ffqm.."."..name) local var, vfqm = find(state.aliases, state.interpreter.global_state.variables, "", ffqm.."."..name)
if var then if var then
set_variable(state, vfqm, v) local s, e = set_variable(state, vfqm, v)
if not s then return nil, e end
return v return v
end end
end end
@ -116,7 +117,7 @@ lua_functions = {
end end
}, },
["_._(r::object, name::string)"] = { ["_._(r::object, name::string)"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(r, n) value = function(r, n)
local state = anselme.running.state local state = anselme.running.state
local obj = r.value local obj = r.value
@ -139,7 +140,7 @@ lua_functions = {
end end
}, },
["_._(r::object, name::string) := v"] = { ["_._(r::object, name::string) := v"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(r, n, v) value = function(r, n, v)
local state = anselme.running.state local state = anselme.running.state
local obj = r.value local obj = r.value
@ -163,13 +164,13 @@ lua_functions = {
}, },
-- index -- index
["()(l::list, i::number)"] = { ["()(l::list, i::number)"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(l, i) value = function(l, i)
return l.value[i.value] or { type = "nil", value = nil } return l.value[i.value] or { type = "nil", value = nil }
end end
}, },
["()(l::list, i::string)"] = { ["()(l::list, i::string)"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(l, i) value = function(l, i)
for _, v in ipairs(l.value) do for _, v in ipairs(l.value) do
if v.type == "pair" and compare(v.value[1], i) then if v.type == "pair" and compare(v.value[1], i) then
@ -183,8 +184,8 @@ lua_functions = {
["()(l::list, i::number) := v"] = { ["()(l::list, i::number) := v"] = {
mode = "raw", mode = "raw",
value = function(l, i, v) value = function(l, i, v)
local lv = l.type == "type" and l.value[1] or l local lv = l.type == "annotated" and l.value[1] or l
local iv = i.type == "type" and i.value[1] or i local iv = i.type == "annotated" and i.value[1] or i
lv.value[iv.value] = v lv.value[iv.value] = v
mark_as_modified(anselme.running.state, lv.value) mark_as_modified(anselme.running.state, lv.value)
return v return v
@ -193,8 +194,8 @@ lua_functions = {
["()(l::list, k::string) := v"] = { ["()(l::list, k::string) := v"] = {
mode = "raw", mode = "raw",
value = function(l, k, v) value = function(l, k, v)
local lv = l.type == "type" and l.value[1] or l local lv = l.type == "annotated" and l.value[1] or l
local kv = k.type == "type" and k.value[1] or k local kv = k.type == "annotated" and k.value[1] or k
-- update index -- update index
for _, x in ipairs(lv.value) do for _, x in ipairs(lv.value) do
if x.type == "pair" and compare(x.value[1], kv) then if x.type == "pair" and compare(x.value[1], kv) then
@ -219,7 +220,7 @@ lua_functions = {
-- bypassed, this case is manually handled in the expression interpreter -- bypassed, this case is manually handled in the expression interpreter
}, },
["_!(fn::variable reference)"] = { ["_!(fn::variable reference)"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(v) value = function(v)
return get_variable(anselme.running.state, v.value) return get_variable(anselme.running.state, v.value)
end end
@ -233,7 +234,7 @@ lua_functions = {
}, },
-- alias -- alias
["alias(ref::function reference, alias::string)"] = { ["alias(ref::function reference, alias::string)"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(ref, alias) value = function(ref, alias)
-- check identifiers -- check identifiers
alias = alias.value alias = alias.value
@ -252,7 +253,7 @@ lua_functions = {
end end
}, },
["alias(ref::variable reference, alias::string)"] = { ["alias(ref::variable reference, alias::string)"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(ref, alias) value = function(ref, alias)
-- check identifiers -- check identifiers
alias = alias.value alias = alias.value
@ -270,20 +271,20 @@ lua_functions = {
}, },
-- pair methods -- pair methods
["name(p::pair)"] = { ["name(p::pair)"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(a) value = function(a)
return a.value[1] return a.value[1]
end end
}, },
["value(p::pair)"] = { ["value(p::pair)"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(a) value = function(a)
return a.value[2] return a.value[2]
end end
}, },
-- list methods -- list methods
["len(l::list)"] = { ["len(l::list)"] = {
mode = "untyped raw", -- raw to count pairs in the list mode = "unannotated raw", -- raw to count pairs in the list
value = function(a) value = function(a)
return { return {
type = "number", type = "number",
@ -294,7 +295,7 @@ lua_functions = {
["insert(l::list, v)"] = { ["insert(l::list, v)"] = {
mode = "raw", mode = "raw",
value = function(l, v) value = function(l, v)
local lv = l.type == "type" and l.value[1] or l local lv = l.type == "annotated" and l.value[1] or l
table.insert(lv.value, v) table.insert(lv.value, v)
mark_as_modified(anselme.running.state, lv.value) mark_as_modified(anselme.running.state, lv.value)
return l return l
@ -303,22 +304,22 @@ lua_functions = {
["insert(l::list, i::number, v)"] = { ["insert(l::list, i::number, v)"] = {
mode = "raw", mode = "raw",
value = function(l, i, v) value = function(l, i, v)
local lv = l.type == "type" and l.value[1] or l local lv = l.type == "annotated" and l.value[1] or l
local iv = i.type == "type" and i.value[1] or i local iv = i.type == "annotated" and i.value[1] or i
table.insert(lv.value, iv.value, v) table.insert(lv.value, iv.value, v)
mark_as_modified(anselme.running.state, lv.value) mark_as_modified(anselme.running.state, lv.value)
return l return l
end end
}, },
["remove(l::list)"] = { ["remove(l::list)"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(l) value = function(l)
mark_as_modified(anselme.running.state, l.value) mark_as_modified(anselme.running.state, l.value)
return table.remove(l.value) return table.remove(l.value)
end end
}, },
["remove(l::list, i::number)"] = { ["remove(l::list, i::number)"] = {
mode = "untyped raw", mode = "unannotated raw",
value = function(l, i) value = function(l, i)
mark_as_modified(anselme.running.state, l.value) mark_as_modified(anselme.running.state, l.value)
return table.remove(l.value, i.value) return table.remove(l.value, i.value)
@ -327,7 +328,7 @@ lua_functions = {
["find(l::list, v)"] = { ["find(l::list, v)"] = {
mode = "raw", mode = "raw",
value = function(l, v) value = function(l, v)
local lv = l.type == "type" and l.value[1] or l local lv = l.type == "annotated" and l.value[1] or l
for i, x in ipairs(lv.value) do for i, x in ipairs(lv.value) do
if compare(x, v) then if compare(x, v) then
return i return i
@ -345,10 +346,10 @@ lua_functions = {
["rand()"] = function() return math.random() end, ["rand()"] = function() return math.random() end,
["rand(a::number)"] = function(a) return math.random(a) end, ["rand(a::number)"] = function(a) return math.random(a) end,
["rand(a::number, b::number)"] = function(a, b) return math.random(a, b) end, ["rand(a::number, b::number)"] = function(a, b) return math.random(a, b) end,
["raw(v)"] = { ["unannotated(v)"] = {
mode = "raw", mode = "raw",
value = function(v) value = function(v)
if v.type == "type" then if v.type == "annotated" then
return v.value[1] return v.value[1]
else else
return v return v
@ -356,19 +357,21 @@ lua_functions = {
end end
}, },
["type(v)"] = { ["type(v)"] = {
mode = "raw", mode = "unannotated raw",
value = function(v) value = function(v)
if v.type == "type" then
return v.value[2]
else
return { return {
type = "string", type = "string",
value = v.type value = v.type
} }
end end
},
["annotation(v::annotated)"] = {
mode = "raw",
value = function(v)
return v.value[2]
end end
}, },
["is of type(v, t)"] = { ["is a(v, t)"] = {
mode = "raw", mode = "raw",
value = function(v, t) value = function(v, t)
return { return {

View file

@ -7,6 +7,7 @@ return [[
~ &pair!alias("paire") ~ &pair!alias("paire")
~ &function reference!alias("réference de fonction") ~ &function reference!alias("réference de fonction")
~ &variable reference!alias("réference de variable") ~ &variable reference!alias("réference de variable")
~ &annotated!alias("annoté")
(Built-in functions) (Built-in functions)
(~ &alias!alias("alias") (~ &alias!alias("alias")
@ -19,8 +20,8 @@ return [[
~ &error!alias("erreur") ~ &error!alias("erreur")
~ &rand!alias("aléa") ~ &rand!alias("aléa")
~ &raw!alias("brut") ~ &raw!alias("brut")
(~ &type!alias("type") ~ &is a!alias("est un")
~ &is of type!alias("est de type") ~ &unannotated!alias("non annoté")
~ &cycle!alias("cycler") ~ &cycle!alias("cycler")
~ &random!alias("aléatoire") ~ &random!alias("aléatoire")
~ &next!alias("séquence") ~ &next!alias("séquence")

View file

@ -136,7 +136,7 @@ types.anselme = {
return { [k] = v } return { [k] = v }
end end
}, },
type = { annotated = {
format = function(val) format = function(val)
local k, ke = format(val[1]) local k, ke = format(val[1])
if not k then return k, ke end if not k then return k, ke end

View file

@ -0,0 +1,11 @@
:weigh::"kg" = 5::"kg"
{weigh}
~ weigh := 12::"kg"
{weigh}
~ weigh := 32
{weigh}

View file

@ -0,0 +1,22 @@
local _={}
_[9]={}
_[8]={}
_[7]={text="12::kg",tags=_[9]}
_[6]={text="5::kg",tags=_[8]}
_[5]={_[7]}
_[4]={_[6]}
_[3]={"error","constraint check failed; while assigning value to variable \"constrained variable assignement.weigh\"; at test/tests/constrained variable assignement.ans:9"}
_[2]={"text",_[5]}
_[1]={"text",_[4]}
return {_[1],_[2],_[3]}
--[[
{ "text", { {
tags = {},
text = "5::kg"
} } }
{ "text", { {
tags = {},
text = "12::kg"
} } }
{ "error", 'constraint check failed; while assigning value to variable "constrained variable assignement.weigh"; at test/tests/constrained variable assignement.ans:9' }
]]--

View file

@ -0,0 +1,7 @@
:weigh::"kg" = 5::"kg"
{weigh}
:not weigh::"kg" = 12
{not weigh}

View file

@ -0,0 +1,14 @@
local _={}
_[5]={}
_[4]={text="5::kg",tags=_[5]}
_[3]={_[4]}
_[2]={"error","constraint check failed; while assigning value to variable \"constrained variable definition.not weigh\"; at test/tests/constrained variable definition.ans:7"}
_[1]={"text",_[3]}
return {_[1],_[2]}
--[[
{ "text", { {
tags = {},
text = "5::kg"
} } }
{ "error", 'constraint check failed; while assigning value to variable "constrained variable definition.not weigh"; at test/tests/constrained variable definition.ans:7' }
]]--