arturo-lang / arturo

Simple, expressive & portable programming language for efficient scripting
http://arturo-lang.io
MIT License
723 stars 32 forks source link

[OOP] Should we allow subtyping "some" of our built-in types? #1357

Open github-actions[bot] opened 11 months ago

github-actions[bot] commented 11 months ago

[OOP] Should we allow subtyping "some" of our built-in types? and if so: what would that mean? GenerateNumericSubtype* = """ init: method [v %TYPE%][ this\value: v ]

https://github.com/arturo-lang/arturo/blob/411617a1906063cf0adfd3ac06804dc4b29403a0/src/helpers/objects.nim#L28

# Arturo
# Programming Language + Bytecode VM compiler
# (c) 2019-2023 Yanis Zafirópulos
#
# @file: helpers/objects.nim
#=======================================================

#=======================================
# Libraries
#=======================================

import std/sequtils, strutils, sugar, tables

import vm/values/[value, comparison]
import vm/values/custom/[vsymbol]

import vm/[exec, errors]

#=======================================
# Constants
#=======================================

const
    ThisRef*            = "this"
    SuperRef*           = "super"

    # TODO(OOP) Should we allow subtyping "some" of our built-in types?
    #  and if so: what would that mean?
    #  labels: open discussion, oop, values
    # GenerateNumericSubtype* = """
    #     init: method [v %TYPE%][
    #         this\value: v
    #     ]

    #     compare: method [that][
    #         (is? %TYPE% that)? -> compare this\value that\value
    #                            -> compare this\value that
    #     ]

    #     add: method [that][
    #         to %TYPE% @[
    #             add this\value (is? %TYPE% that)? -> that\value -> that
    #         ]
    #     ]

    #     sub: method [that][
    #         to %TYPE% @[
    #             sub this\value (is? %TYPE% that)? -> that\value -> that
    #         ]
    #     ]

    #     mul: method [that][
    #         to %TYPE% @[
    #             mul this\value (is? %TYPE% that)? -> that\value -> that
    #         ]
    #     ]

    #     div: method [that][
    #         to %TYPE% @[
    #             div this\value (is? %TYPE% that)? -> that\value -> that
    #         ]
    #     ]

    #     fdiv: method [that][
    #         to %TYPE% @[
    #             fdiv this\value (is? %TYPE% that)? -> that\value -> that
    #         ]
    #     ]

    #     mod: method [that][
    #         to %TYPE% @[
    #             mod this\value (is? %TYPE% that)? -> that\value -> that
    #         ]
    #     ]

    #     pow: method [that][
    #         to %TYPE% @[
    #             pow this\value (is? %TYPE% that)? -> that\value -> that
    #         ]
    #     ]

    #     inc: method [][
    #         to %TYPE% @[
    #             inc this\value
    #         ]
    #     ]

    #     dec: method [][
    #         to %TYPE% @[
    #             dec this\value
    #         ]
    #     ]

    #     neg: method [][
    #         to %TYPE% @[
    #             neg this\value
    #         ]
    #     ]

    #     string: method []-> to :string this\value
    #     integer: method []-> to :integer this\value
    #     floating: method []-> to :floating this\value
    #     rational: method []-> to :rational this\value
    #     complex: method []-> to :complex this\value
    #     quantity: method []-> to :quantity this\value
    # """

#=======================================
# Helpers
#=======================================

template checkArguments(pr: Prototype, values: ValueArray | ValueDict) =
    when values is ValueArray:
        if pr.fields.len != values.len:
            RuntimeError_IncorrectNumberOfArgumentsForInitializer(pr.name, values.len, toSeq(pr.fields.keys))
    else:
        if (pr.fields.len != 0 or (pr.fields.len == 0 and pr.content.hasKey($ConstructorM))) and pr.fields.len != values.len:
            RuntimeError_IncorrectNumberOfArgumentsForInitializer(pr.name, values.len, toSeq(pr.fields.keys))

proc fetchConstructorArguments(pr: Prototype, values: ValueArray | ValueDict, args: var ValueArray): bool =
    result = true

    when values is ValueArray:
        for v in values:
            args.add(v)
    else:
        if pr.fields.len == 0:
            return false
        else:
            for k,v in pr.fields:
                if k != ThisRef:
                    if (let vv = values.getOrDefault(k, nil); not vv.isNil):
                        args.add(vv)
                    else:
                        RuntimeError_MissingArgumentForInitializer(pr.name, k)

# TODO(Helpers/objects) Should check defined magic methods for validity
#  obviously, we cannot check everything beforehand (if the parems are correct
#  or if the method return what it should - or if it returns at all, for that
#  matter). But we should - at least - check if the given magic method has the
#  correct number of arguments. And if not, throw an error.
#  labels: oop, error handling
func processMagic(mm: var MagicMethods, methodName: string, target: Value) {.inline.} =
    try:
        let mgk = parseEnum[MagicMethod](methodName)
        mm[mgk] = proc (args: ValueArray) =
            callMethod(target, "\\" & methodName, args)
    except:
        discard

#=======================================
# Iterators
#=======================================

iterator objectKeys*(vd: ValueDict): string =
    for k,v in vd:
        if v.kind != Method:
            yield k

iterator objectMethods*(vd: ValueDict): string =
    for k,v in vd:
        if v.kind == Method:
            yield k

iterator objectValues*(vd: ValueDict): Value =
    for _,v in vd:
        if v.kind != Method:
            yield v

iterator objectPairs*(vd: ValueDict): (string, Value) =
    for k,v in vd:
        if v.kind != Method:
            yield (k,v)

#=======================================
# Methods
#=======================================

func flattenedObject*(vd: ValueDict): ValueArray =
    for k,v in vd.objectPairs:
        result.add(newString(k))
        result.add(v)

func objectSize*(vd: ValueDict): int =
    for v in vd.objectValues:
        if v.kind != Method:
            result += 1

func generatedConstructor*(params: ValueArray): Value {.inline.} =
    if params.len > 0 and params.all((x) => x.kind in {Word, Literal, String, Type}):
        let constructorBody = newBlock()
        for val in params:
            if val.kind in {Word, Literal, String}:
                constructorBody.a.add(@[
                    newPathLabel(@[newWord(ThisRef), newWord(val.s)]),
                    newWord(val.s)
                ])

        return newMethodFromDefinition(params, constructorBody)

    return nil

func generatedCompare*(key: Value): Value {.inline.} =
    let compareBody = newBlock(@[
        newWord("if"), newPath(@[newWord(ThisRef), key]), newSymbol(greaterthan), newPath(@[newWord("that"), key]), newBlock(@[newWord("return"),newInteger(1)]),
        newWord("if"), newPath(@[newWord(ThisRef), key]), newSymbol(equal), newPath(@[newWord("that"), key]), newBlock(@[newWord("return"),newInteger(0)]),
        newWord("return"), newWord("neg"), newInteger(1)
    ])

    return newMethodFromDefinition(@[newWord("that")], compareBody)

proc getFieldTable*(defs: ValueDict): ValueDict {.inline.} =
    result = newOrderedTable[string,Value]()

    if (let constructorMethod = defs.getOrDefault($ConstructorM, nil); not constructorMethod.isNil):
        for p in constructorMethod.params:
            if p != "this":
                result[p] = newType(Any)

        let ensureW = newWord("ensure")
        var i = 1
        while i < constructorMethod.main.a.len - 1:
            if (let ensureBlock = constructorMethod.main.a[i+1]; constructorMethod.main.a[i] == ensureW and ensureBlock.kind == Block):
                let lastElement = ensureBlock.a[^1]
                if lastElement.kind == Word:
                    if ensureBlock.a[^2].kind == Type:
                        result[lastElement.s] = ensureBlock.a[^2]
                elif lastElement.kind == Block:
                    let sublastElement = lastElement.a[^1]
                    if sublastElement.kind == Word and lastElement.a[^2].kind == Type:
                        result[sublastElement.s] = newBlock(lastElement.a.filter((x) => x.kind == Type))
            i += 2

proc injectThis*(fun: Value) {.inline.} =
    if fun.params.len < 1 or fun.params[0] != ThisRef:
        fun.params.insert(ThisRef)
        fun.arity += 1

proc uninjectingThis*(fun: Value): Value {.inline.} =
    result = copyValue(fun)
    if result.params.len >= 1 and result.params[0] == ThisRef:
        result.params.delete(0..0)
        result.arity -= 1

proc injectingSuper*(fun: Value, super: Value): Value {.inline.} =
    result = copyValue(fun)

    let injection = @[
        newLabel(SuperRef), newWord("function"), newBlock(super.mparams.map((w)=>newWord(w))), super.mmain
    ]

    result.main.a.insert(injection)

proc generateNewObject*(pr: Prototype, values: ValueArray | ValueDict): Value =
    # create basic object
    result = newObject(pr)

    # migrate all content and 
    # process magic method accordingly
    var magicMethods = MagicMethods()
    for k,v in pr.content:
        result.o[k] = copyValue(v)
        if v.kind == Method and not v.mdistinct:
            processMagic(magicMethods, k, v)

    # verify arguments
    checkArguments(pr, values)

    # and process them accordingly
    var args: ValueArray = @[]
    if not fetchConstructorArguments(pr, values, args):
        when values is ValueDict:
            # if there is no constructor defined
            # and we try to initialize the object with a dictionary,
            # let's just copy the values
            for k,v in values:
                result.o[k] = v

    # perform initialization 
    # using the available constructor
    if (let constructor = magicMethods.getOrDefault(ConstructorM, nil); not constructor.isNil):
        args.insert(result)
        constructor(args)

    # embed magic methods as well
    result.magic = magicMethods
 No newline at end of file
ndex 130a081918..a3339e228c 100644
++ b/src/library/Arithmetic.nim

8ce34ce0d713c6c74c7970f16589eeb82c0baa0f

drkameleon commented 11 months ago

Basically, the idea is that e.g.

define :myNumber is :integer []

would auto-generate a type as if we had done:

define :myNumber [
    init: method [num :integer][
        this\value: num
    ]

    add: method [that][
        to :myNumber @[
             add this\value (is? :myNumber that)? -> that\value -> that
        ]
    ]

    ; add the rest of the magic methods as shown above here
]

So, we could do sth like:

a: to :myNumber [10]
print a + 5 ; would print 15

Also, note: I'm planning to allow the conversion to given user-type from other type of params as well. For example, if a type's constructor takes one :integer, why not allow for sth like to :myNumber 10 and forcefully require to :myNumber [10]. I think it'd make sense ;)

stale[bot] commented 3 months ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.