Skip to content

Commit

Permalink
Support using <forwarddecl> with functions
Browse files Browse the repository at this point in the history
This feature is useful to allow to call functions before defining them
without runtime overhead
  • Loading branch information
edubart committed Aug 4, 2021
1 parent 062b8d6 commit e6727dd
Show file tree
Hide file tree
Showing 6 changed files with 87 additions and 29 deletions.
69 changes: 46 additions & 23 deletions nelua/analyzer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -601,7 +601,7 @@ function visitors.Annotation(context, node, opts)
end
elseif name == 'codename' then
objattr.fixedcodename = params
elseif name == 'cincomplete' or name =='forwarddecl' then
elseif istypedecl and (name == 'cincomplete' or name =='forwarddecl') then
objattr.size = nil
objattr.bitsize = nil
objattr.align = nil
Expand Down Expand Up @@ -1655,7 +1655,7 @@ local function visitor_Type_MetaFieldIndex(context, node, objtype, name)
end
symbol.anonymous = true
symbol.scope = context.rootscope
elseif infuncdef or inglobaldecl then
elseif (infuncdef or inglobaldecl) and not symbol.forwarddecl then
if symbol.node ~= node then
node:raisef("cannot redefine meta type field '%s' in record '%s'", name, objtype)
end
Expand Down Expand Up @@ -2534,21 +2534,17 @@ function visitors.DoExpr(context, node)
end

local function visitor_FuncDef_variable(context, declscope, varnode)
local decl = not not declscope
if declscope == 'global' then
if not context.scope.is_topscope then
varnode:raisef("global function can only be declared in top scope")
end
varnode.attr.global = true
end
if declscope == 'global' or context.scope.is_topscope or decl then
if declscope == 'global' or context.scope.is_topscope or declscope then
varnode.attr.staticstorage = true
end
local symbol = context:traverse_node(varnode)
if symbol and symbol.metafunc then
decl = true
end
return symbol, decl
return symbol
end

local function visitor_function_arguments(context, symbol, selftype, argnodes, checkpoly)
Expand All @@ -2565,7 +2561,7 @@ local function visitor_function_arguments(context, symbol, selftype, argnodes, c
local off = 0

if selftype then -- inject 'self' type as first argument
local selfsym = symbol.selfsym
local selfsym = funcscope.selfsym
if not selfsym then
selfsym = Symbol{
name = 'self',
Expand All @@ -2574,7 +2570,7 @@ local function visitor_function_arguments(context, symbol, selftype, argnodes, c
type = selftype,
scope = funcscope,
}
symbol.selfsym = selfsym
funcscope.selfsym = selfsym
end
argattrs[1] = selfsym
funcscope:add_symbol(selfsym)
Expand Down Expand Up @@ -2653,7 +2649,7 @@ local function visitor_function_returns(context, node, retnodes, ispolyparent)
return rettypes, hasauto
end

local function visitor_function_annotations(context, node, annotnodes, blocknode, symbol, type)
local function visitor_function_annotations(context, node, annotnodes, blocknode, symbol, type, defn)
if annotnodes then
context:traverse_nodes(annotnodes, {symbol=symbol})
end
Expand All @@ -2662,9 +2658,9 @@ local function visitor_function_annotations(context, node, annotnodes, blocknode

do -- handle attributes and annotations
-- annotation cimport
if attr.cimport then
if attr.cimport or (attr.forwarddecl and not defn) then
if #blocknode ~= 0 then
blocknode:raisef("body of an import function must be empty")
blocknode:raisef("body of a function declaration must be empty")
end
if attr.codename == 'nelua_main' then
context.hookmain = true
Expand Down Expand Up @@ -2698,7 +2694,7 @@ local function visitor_function_sideeffect(attr, functype, funcscope)
if functype and not attr.nosideeffect then
-- C imported function has side effects unless told otherwise,
-- if any side effect call or upvalue assignment is detected the function also has side effects
if attr.cimport or funcscope.sideeffect then
if (attr.cimport or attr.forwarddecl) or funcscope.sideeffect then
functype.sideeffect = true
else
functype.sideeffect = false
Expand Down Expand Up @@ -2782,6 +2778,9 @@ local function resolve_function_type(node, symbol, varnode, varsym, decl, argatt
local attr = node.attr
if ispolyparent then
assert(not polysymbol)
if symbol.forwarddecl then
node:raisef("polymorphic functions cannot be forward declared")
end
type = types.PolyFunctionType(argattrs, rettypes, node)
else
type = types.FunctionType(argattrs, rettypes, node)
Expand Down Expand Up @@ -2814,7 +2813,7 @@ function visitors.FuncDef(context, node, opts)

local type = node.attr.ftype
context:push_forked_state{infuncdef = node, inpolydef = polysymbol}
local varsym, decl = visitor_FuncDef_variable(context, declscope, varnode)
local varsym = visitor_FuncDef_variable(context, declscope, varnode)
local attr, symbol
if varsym then -- symbol may be nil in case of array/dot index
symbol = varsym
Expand All @@ -2840,17 +2839,41 @@ function visitors.FuncDef(context, node, opts)
context:pop_state()

-- we must know if the symbols is going to be polymorphic
local forwarddecl
if annotnodes then
for i=1,#annotnodes do
local annotname = annotnodes[i][1]
if annotname == 'polymorphic' then
attr.polymorphic = true
elseif annotname == 'cimport' then
attr.cimport = true
elseif annotname == 'forwarddecl' then
forwarddecl = true
attr.forwarddecl = true
end
end
end

-- detect if is a function declaration/definition
local decl = not not declscope or forwarddecl
local defn = not (attr.nodecl or attr.cimport or attr.hookmain or forwarddecl)
if symbol then
if not forwarddecl and symbol.forwarddecl then
defn = true
decl = false
elseif symbol.metafunc then
decl = true
end
if decl then
node.funcdecl = true
symbol.funcdeclared = true
end
if defn then
node.funcdefn = true
symbol.funcdefined = true
end
end

-- detect the self type
local selftype
if varnode.is_ColonIndex then
Expand Down Expand Up @@ -2881,6 +2904,7 @@ function visitors.FuncDef(context, node, opts)

-- traverse the function arguments
argattrs, ispolyparent = visitor_function_arguments(context, symbol, selftype, argnodes, not polysymbol)

symbol.argattrs = argattrs

-- traverse the function returns
Expand All @@ -2893,12 +2917,10 @@ function visitors.FuncDef(context, node, opts)
end

-- traverse annotation nodes
visitor_function_annotations(context, node, annotnodes, blocknode, symbol, type)

visitor_function_annotations(context, node, annotnodes, blocknode, symbol, type, defn)
-- traverse the function block
if not ispolyparent then -- poly functions never traverse the blocknode by itself
context:traverse_node(blocknode)

-- after traversing we should know auto types
if hasautoret then
if not type and rettypes and types.are_types_resolved(rettypes) then
Expand All @@ -2908,18 +2930,19 @@ function visitors.FuncDef(context, node, opts)
node:raisef("a function return is set to 'auto', but the function never returns")
end
end
end

visitor_function_sideeffect(attr, type, funcscope)
if defn then
visitor_function_sideeffect(attr, type, funcscope)
end
end

local resolutions_count = funcscope:resolve()
context:pop_state()
context:pop_scope()
until resolutions_count == 0

-- type checking for returns
if type and type.is_procedure and not type.is_polyfunction and rettypes and #rettypes > 0 and
not(attr.nodecl or attr.cimport or attr.hookmain) then
if type and defn and type.is_function and rettypes and #rettypes > 0 then
local canbeempty = tabler.iallfield(rettypes, 'is_nilable')
if not canbeempty and not block_endswith_return(blocknode) then
node:raisef("a return statement is missing before function end")
Expand Down Expand Up @@ -2986,7 +3009,7 @@ function visitors.Function(context, node)
end

-- traverse annotation nodes
visitor_function_annotations(context, node, annotnodes, blocknode, symbol, type)
visitor_function_annotations(context, node, annotnodes, blocknode, symbol, type, true)

-- traverse the function block
context:traverse_node(blocknode)
Expand Down
2 changes: 2 additions & 0 deletions nelua/astnode.lua
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ ASTNode.baseshape = shaper.shape{
checked = shaper.boolean:is_optional(),
-- Scope where the node is defined.
scope = shaper.scope:is_optional(),
funcdefn = shaper.boolean:is_optional(),
funcdecl = shaper.boolean:is_optional(),
}

-- Tag for a generic ASTNode.
Expand Down
11 changes: 6 additions & 5 deletions nelua/cgenerator.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1214,15 +1214,18 @@ function visitors.FuncDef(context, node, emitter)
context:ensure_builtin(attr.codename) -- ensure the builtin is declared and defined
return -- nothing more to do
end
local mustdecl, mustdefn = not attr.nodecl, not attr.cimport
local mustdecl, mustdefn = node.funcdecl and not attr.nodecl, node.funcdefn
if attr.forwarddecl and not attr.funcdefined then
node:raisef("function '%s' marked as forward declaration but was never defined", attr)
end
if not (mustdecl or mustdefn) then -- do we need to declare or define?
return -- nothing to do
end
-- lets declare or define the function
local varscope, varnode, argnodes, blocknode = node[1], node[2], node[3], node[6]
local varnode, argnodes, blocknode = node[2], node[3], node[6]
local funcname = varnode
-- handle function variable assignment
if not varscope then
if not attr.funcdeclared then
if varnode.is_Id then
funcname = context.rootscope:generate_name(context:declname(varnode.attr))
emitter:add_indent_ln(varnode, ' = ', funcname, ';')
Expand All @@ -1231,8 +1234,6 @@ function visitors.FuncDef(context, node, emitter)
if objtype.is_record then
funcname = context.rootscope:generate_name(objtype.codename..'_funcdef_'..fieldname)
emitter:add_indent_ln(varnode, ' = ', funcname, ';')
else
assert(objtype.is_type)
end
end
end
Expand Down
3 changes: 3 additions & 0 deletions nelua/typedefs.lua
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ typedefs.function_annots = {
polymorphic = true,
-- Force a polymorphic function to always be evaluated.
alwayseval = true,
-- Mark a function for forward declaration.
-- This allows to call a function before defining it.
forwarddecl = true,
}

-- List of possible annotations for variables.
Expand Down
18 changes: 18 additions & 0 deletions spec/cgenerator_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -3396,6 +3396,24 @@ it("forward type declaration", function()
local Union <forwarddecl> = @union{}
local Union = @union{i: integer, n: number}
]=])
expect.run_c([=[
local function f(x: integer): integer <forwarddecl> end
assert(f(1) == 1)
function f(x: integer): integer return x end
local Foo = @record{x: integer}
function Foo.f(x: integer): integer <forwarddecl> end
function Foo:g(x: integer): integer <forwarddecl> end
assert(Foo.f(1) == 1)
local foo: Foo = {1}
assert(foo:g(1) == 2)
function Foo.f(x: integer): integer return x end
function Foo:g(x: integer): integer return self.x + x end
]=])
expect.run_error_c([=[
local function f(x: integer): integer <forwarddecl> end
assert(f(1) == 1)
]=], "marked as forward declaration but was never defined")
end)

it("function assignment", function()
Expand Down
13 changes: 12 additions & 1 deletion spec/typechecker_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -1636,7 +1636,7 @@ it("annotations", function()
expect.analyze_ast("local Record: type <aligned(8)> = @record{x: integer}")
expect.analyze_error(
"local function f() <cimport,nodecl> return 0 end",
"body of an import function must be empty")
"body of a function declaration must be empty")
expect.analyze_error("local a <cimport>", "must have an explicit type")
expect.analyze_error("local a: integer <cimport,nodecl> = 2", "cannot assign imported variables")
expect.analyze_error([[
Expand Down Expand Up @@ -1967,6 +1967,17 @@ it("forward type declaration", function()
"cannot be of forward declared type")
end)

it("forward function declaration", function()
expect.analyze_ast("local function f() <forwarddecl> end; function f() end")
expect.analyze_ast("local function f(x: byte): void <forwarddecl> end; function f(x: byte): void end")
expect.analyze_error(
"local function f(x: auto): auto <forwarddecl> end function f() end",
"polymorphic functions cannot be forward declared")
expect.analyze_error(
"local function f(x: byte): void <forwarddecl> print(1) end",
"body of a function declaration must be empty")
end)

it("using annotation", function()
expect.analyze_ast([[
local MyEnum <using> = @enum{
Expand Down

1 comment on commit e6727dd

@edubart
Copy link
Owner Author

@edubart edubart commented on e6727dd Aug 4, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clearly inspired by C's and C++'s forward declaration which we all use, one way or another!

Indeed, this also solve the problem of circular function dependency.

Can we add a simple example in overview.nelua?

It's on my TODO to add it in the overview sometime in the future, I usually don't update new features in overview right away, because they haven't been tested much yet, thus there might be hidden bugs or may even change syntax or semantics until stable. Overview usually have more stable features that users can trust.

Please sign in to comment.