feat(oop): METHOD ... INLINE <expr> and MESSAGE handlers
Harbour's inline-method sugar was parsed but the body was skipped, leaving any `METHOD X() INLINE expr` declaration registered in the class vtable with no matching HB_<CLASS>_X function — link error at build time. Parser: MethodDecl gains an InlineBody Expr field. parseClassMethodDecl captures the expression after INLINE instead of skipping to EOL. New parseMessageDecl handles `MESSAGE <name> [(params)] INLINE expr` and returns the same MethodDecl shape. Codegen: emitClassDecl walks members a second time after the class registration init block and emits emitInlineMethodBody for each IsInline method — a Frame(nParams, 0) + emitExpr(InlineBody) + RetValue function. curMethodClass is bound so ::super: inside an inline body still resolves. Tested (/tmp/test_inline.prg): all four patterns — bare INLINE, MESSAGE INLINE, INLINE with params, INLINE reading ::field — produce expected values. FiveSql2 43/43, Harbour compat 56/56, Go test ALL PASS. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -177,6 +177,7 @@ type MethodDecl struct {
|
|||||||
ClassName string // METHOD name CLASS classname (standalone)
|
ClassName string // METHOD name CLASS classname (standalone)
|
||||||
Params []*ParamDecl
|
Params []*ParamDecl
|
||||||
IsInline bool // INLINE method
|
IsInline bool // INLINE method
|
||||||
|
InlineBody Expr // inline expression body — `RETURN <expr>` equivalent
|
||||||
IsSetGet bool // METHOD name(x) SETGET — getter if no arg, setter if arg
|
IsSetGet bool // METHOD name(x) SETGET — getter if no arg, setter if arg
|
||||||
IsAccess bool // ACCESS name METHOD getterName
|
IsAccess bool // ACCESS name METHOD getterName
|
||||||
IsAssign bool // ASSIGN name METHOD setterName
|
IsAssign bool // ASSIGN name METHOD setterName
|
||||||
|
|||||||
@@ -74,6 +74,18 @@ func (g *Generator) emitClassDecl(cls *ast.ClassDecl) {
|
|||||||
g.writeln("}")
|
g.writeln("}")
|
||||||
g.writeln("")
|
g.writeln("")
|
||||||
|
|
||||||
|
// Emit function bodies for INLINE methods — the class-body form
|
||||||
|
// `METHOD X() INLINE expr` / `MESSAGE X INLINE expr` carries its
|
||||||
|
// own body, unlike plain METHOD declarations which expect a
|
||||||
|
// standalone `METHOD X() CLASS Foo` implementation elsewhere.
|
||||||
|
for _, m := range cls.Members {
|
||||||
|
md, ok := m.(*ast.MethodDecl)
|
||||||
|
if !ok || !md.IsInline || md.InlineBody == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
g.emitInlineMethodBody(className, md)
|
||||||
|
}
|
||||||
|
|
||||||
// Also need a constructor function: Person() returns new object
|
// Also need a constructor function: Person() returns new object
|
||||||
// This is called as Person():New(...)
|
// This is called as Person():New(...)
|
||||||
g.writeln(fmt.Sprintf("func HB_%s_CTOR(t *hbrt.Thread) {", className))
|
g.writeln(fmt.Sprintf("func HB_%s_CTOR(t *hbrt.Thread) {", className))
|
||||||
@@ -89,6 +101,40 @@ func (g *Generator) emitClassDecl(cls *ast.ClassDecl) {
|
|||||||
// Constructor symbol already added in Generate() symbol collection phase
|
// Constructor symbol already added in Generate() symbol collection phase
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// emitInlineMethodBody generates the HB_<CLASS>_<METHOD> function for
|
||||||
|
// an INLINE-declared method: the body is the single expression parsed
|
||||||
|
// after the INLINE keyword, evaluated and returned. Params bind to
|
||||||
|
// locals 1..N so the inline expression can reference them.
|
||||||
|
func (g *Generator) emitInlineMethodBody(className string, md *ast.MethodDecl) {
|
||||||
|
methodName := strings.ToUpper(md.Name)
|
||||||
|
goFuncName := fmt.Sprintf("HB_%s_%s", className, methodName)
|
||||||
|
nParams := len(md.Params)
|
||||||
|
|
||||||
|
g.writeln(fmt.Sprintf("func %s(t *hbrt.Thread) {", goFuncName))
|
||||||
|
g.indent++
|
||||||
|
g.writeln(fmt.Sprintf("t.Frame(%d, 0)", nParams))
|
||||||
|
g.writeln("defer t.EndProc()")
|
||||||
|
|
||||||
|
// Param name → local index map so the inline expr can reference them.
|
||||||
|
localMap := make(localMap)
|
||||||
|
for i, p := range md.Params {
|
||||||
|
localMap[strings.ToUpper(p.Name)] = i + 1
|
||||||
|
}
|
||||||
|
prevLocals := g.curLocals
|
||||||
|
prevCls := g.curMethodClass
|
||||||
|
g.curLocals = localMap
|
||||||
|
g.curMethodClass = className
|
||||||
|
|
||||||
|
g.emitExpr(md.InlineBody)
|
||||||
|
g.writeln("t.RetValue()")
|
||||||
|
|
||||||
|
g.curLocals = prevLocals
|
||||||
|
g.curMethodClass = prevCls
|
||||||
|
g.indent--
|
||||||
|
g.writeln("}")
|
||||||
|
g.writeln("")
|
||||||
|
}
|
||||||
|
|
||||||
// emitMethodDeclStandalone generates a standalone METHOD ... CLASS ... implementation.
|
// emitMethodDeclStandalone generates a standalone METHOD ... CLASS ... implementation.
|
||||||
func (g *Generator) emitMethodDeclStandalone(md *ast.MethodDecl) {
|
func (g *Generator) emitMethodDeclStandalone(md *ast.MethodDecl) {
|
||||||
if md.ClassName == "" {
|
if md.ClassName == "" {
|
||||||
|
|||||||
@@ -610,8 +610,14 @@ func (p *Parser) parseClassDecl() *ast.ClassDecl {
|
|||||||
} else {
|
} else {
|
||||||
p.skipToEndOfLine()
|
p.skipToEndOfLine()
|
||||||
}
|
}
|
||||||
|
} else if upper == "MESSAGE" {
|
||||||
|
// MESSAGE <name> [(params)] INLINE <expr>
|
||||||
|
// Harbour sugar for an inline method. Emit as MethodDecl so
|
||||||
|
// emitClassDecl routes it through AddMethod and the inline
|
||||||
|
// body emitter generates the HB_<CLASS>_<NAME> function.
|
||||||
|
members = append(members, p.parseMessageDecl())
|
||||||
} else if upper == "ON" || upper == "OPERATOR" || upper == "DESTRUCTOR" ||
|
} else if upper == "ON" || upper == "OPERATOR" || upper == "DESTRUCTOR" ||
|
||||||
upper == "DELEGATE" || upper == "ERROR" || upper == "MESSAGE" ||
|
upper == "DELEGATE" || upper == "ERROR" ||
|
||||||
upper == "VIRTUAL" || upper == "DEFERRED" {
|
upper == "VIRTUAL" || upper == "DEFERRED" {
|
||||||
// ON ERROR, OPERATOR "+" ARG, DESTRUCTOR, DELEGATE — skip to EOL
|
// ON ERROR, OPERATOR "+" ARG, DESTRUCTOR, DELEGATE — skip to EOL
|
||||||
p.skipToEndOfLine()
|
p.skipToEndOfLine()
|
||||||
@@ -740,21 +746,74 @@ func (p *Parser) parseClassMethodDecl() *ast.MethodDecl {
|
|||||||
p.skipToEndOfLine()
|
p.skipToEndOfLine()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip INLINE + rest of line (METHOD ... INLINE expr)
|
// INLINE expression — parse as the method body instead of skipping.
|
||||||
p.skipClassInline()
|
// Harbour semantics: `METHOD X() INLINE expr` is sugar for a method
|
||||||
|
// whose body is `RETURN expr`.
|
||||||
|
isInline := false
|
||||||
|
var inlineBody ast.Expr
|
||||||
|
if p.current.Kind == token.INLINE_KW ||
|
||||||
|
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
|
||||||
|
p.advance() // consume INLINE
|
||||||
|
isInline = true
|
||||||
|
inlineBody = p.parseExpr()
|
||||||
|
}
|
||||||
|
|
||||||
p.expectEndOfStmt()
|
p.expectEndOfStmt()
|
||||||
|
|
||||||
return &ast.MethodDecl{
|
return &ast.MethodDecl{
|
||||||
MethodPos: methodPos,
|
MethodPos: methodPos,
|
||||||
Name: name,
|
Name: name,
|
||||||
Params: params,
|
Params: params,
|
||||||
IsSetGet: isSetGet,
|
IsSetGet: isSetGet,
|
||||||
EndPos: methodPos,
|
IsInline: isInline,
|
||||||
|
InlineBody: inlineBody,
|
||||||
|
EndPos: methodPos,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// skipClassInline skips INLINE keyword and the rest of the line (used in CLASS body)
|
// parseMessageDecl parses `MESSAGE <name> [(params)] INLINE <expr>` in
|
||||||
|
// a CLASS body and returns a MethodDecl. Harbour semantics: a MESSAGE
|
||||||
|
// handler is invoked like a method and behaves identically — the form
|
||||||
|
// exists mostly for readability of "this ident is really a message
|
||||||
|
// handler, not a regular method". Without INLINE the handler is
|
||||||
|
// declaration-only (the real body arrives as a separate
|
||||||
|
// `METHOD <name>() CLASS X` implementation).
|
||||||
|
func (p *Parser) parseMessageDecl() *ast.MethodDecl {
|
||||||
|
msgPos := p.current.Pos
|
||||||
|
p.advance() // MESSAGE
|
||||||
|
|
||||||
|
name := p.expectMethodName().Literal
|
||||||
|
|
||||||
|
var params []*ast.ParamDecl
|
||||||
|
if p.match(token.LPAREN) {
|
||||||
|
params = p.parseParamList()
|
||||||
|
p.expect(token.RPAREN)
|
||||||
|
}
|
||||||
|
|
||||||
|
isInline := false
|
||||||
|
var inlineBody ast.Expr
|
||||||
|
if p.current.Kind == token.INLINE_KW ||
|
||||||
|
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
|
||||||
|
p.advance()
|
||||||
|
isInline = true
|
||||||
|
inlineBody = p.parseExpr()
|
||||||
|
}
|
||||||
|
|
||||||
|
p.expectEndOfStmt()
|
||||||
|
|
||||||
|
return &ast.MethodDecl{
|
||||||
|
MethodPos: msgPos,
|
||||||
|
Name: name,
|
||||||
|
Params: params,
|
||||||
|
IsInline: isInline,
|
||||||
|
InlineBody: inlineBody,
|
||||||
|
EndPos: msgPos,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// skipClassInline skips INLINE keyword and the rest of the line.
|
||||||
|
// Used by ACCESS/ASSIGN decls where inline body handling hasn't been
|
||||||
|
// wired up yet (falls back to pre-INLINE-capture behaviour).
|
||||||
func (p *Parser) skipClassInline() {
|
func (p *Parser) skipClassInline() {
|
||||||
if p.current.Kind == token.INLINE_KW ||
|
if p.current.Kind == token.INLINE_KW ||
|
||||||
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
|
(p.current.Kind == token.IDENT && p.currentUpper() == "INLINE") {
|
||||||
|
|||||||
Reference in New Issue
Block a user