diff --git a/src/checker/check2.h b/src/checker/check2.h index b8f4300..bd0e64d 100644 --- a/src/checker/check2.h +++ b/src/checker/check2.h @@ -34,7 +34,7 @@ namespace ts::vm2 { case TypeKind::TupleMember: { if (left->kind != TypeKind::TupleMember) return false; //todo: handle optional - if (!extends((Type *)left->type, (Type *)right->type)) return false; + if (!extends((Type *) left->type, (Type *) right->type)) return false; return true; } @@ -168,6 +168,13 @@ namespace ts::vm2 { } break; } + case TypeKind::Parameter: { + switch (left->kind) { + case TypeKind::Parameter: + return extends((Type *) left->type, (Type *) right->type); + } + return extends(left, (Type *) right->type); + } } return false; } diff --git a/src/checker/compiler.h b/src/checker/compiler.h index 2eb0d74..c78a872 100644 --- a/src/checker/compiler.h +++ b/src/checker/compiler.h @@ -996,15 +996,14 @@ namespace ts::checker { //which indicates the VM that the function needs to be instantiated first. auto subroutineIndex = program.pushSubroutineNameLess(); - + unsigned int size = 0; for (auto &¶m: n->typeParameters->list) { handle(param, program); } program.pushSlots(); - for (auto &¶m: n->parameters->list) { - handle(param, program); - } + //first comes the return type + size++; if (n->type) { handle(n->type, program); } else { @@ -1014,7 +1013,13 @@ namespace ts::checker { } else { } } + + for (auto &¶m: n->parameters->list) { + size++; + handle(param, program); + } program.pushOp(OP::Function, node); + program.pushUint16(size); program.popSubroutine(); program.pushOp(OP::FunctionRef, node); @@ -1024,9 +1029,9 @@ namespace ts::checker { program.pushSubroutine(symbol); program.pushSlots(); - for (auto &¶m: n->parameters->list) { - handle(param, program); - } + unsigned int size = 0; + //first comes the return type + size++; if (n->type) { handle(n->type, program); } else { @@ -1036,7 +1041,14 @@ namespace ts::checker { } else { } } + + for (auto &¶m: n->parameters->list) { + size++; + handle(param, program); + } + program.pushOp(OP::Function, node); + program.pushUint16(size); program.popSubroutine(); } } diff --git a/src/checker/debug.h b/src/checker/debug.h index cd29e7b..b8b26a0 100644 --- a/src/checker/debug.h +++ b/src/checker/debug.h @@ -163,10 +163,11 @@ namespace ts::checker { break; } case OP::CallExpression: { - params += fmt::format(" &{}", vm::readUint16(bin, i + 1)); + params += fmt::format(" {}", vm::readUint16(bin, i + 1)); vm::eatParams(op, &i); break; } + case OP::Function: case OP::Union: case OP::Tuple: case OP::TemplateLiteral: diff --git a/src/checker/types2.h b/src/checker/types2.h index 7501c73..6d08040 100644 --- a/src/checker/types2.h +++ b/src/checker/types2.h @@ -19,6 +19,7 @@ namespace ts::vm2 { Undefined, String, Number, + BigInt, Boolean, Literal, PropertySignature, @@ -29,6 +30,9 @@ namespace ts::vm2 { Tuple, TupleMember, TemplateLiteral, + Parameter, + Function, + FunctionRef, }; //Used in the vm @@ -38,11 +42,12 @@ namespace ts::vm2 { StringLiteral = 1 << 2, NumberLiteral = 1 << 3, BooleanLiteral = 1 << 4, - True = 1 << 5, - False = 1 << 6, - Stored = 1 << 6, //Used somewhere as cache or as value (subroutine->result for example), and thus can not be stolen/modified - RestReuse = 1 << 8, //allow to reuse/steal T in ...T - Deleted = 1 << 9, //for debugging purposes + BigIntLiteral = 1 << 5, + True = 1 << 6, + False = 1 << 7, + Stored = 1 << 8, //Used somewhere as cache or as value (subroutine->result for example), and thus can not be stolen/modified + RestReuse = 1 << 9, //allow to reuse/steal T in ...T + Deleted = 1 << 10, //for debugging purposes }; struct Type; @@ -285,6 +290,10 @@ namespace ts::vm2 { stringifyType((Type *) type->type, r); break; } + case TypeKind::Parameter: { + stringifyType((Type *) type->type, r); + break; + } case TypeKind::Tuple: { r += "["; auto current = (TypeRef *) type->type; @@ -309,9 +318,9 @@ namespace ts::vm2 { r += "..."; break; } - if (current) r += "| "; stringifyType(current->type, r); current = current->next; + if (current) r += " | "; } break; } @@ -359,6 +368,10 @@ namespace ts::vm2 { return r; } + inline bool isOptional(Type *type) { + return type->flag & TypeFlag::Optional; + } + // struct TypeMeta { // string_view typeName; // }; diff --git a/src/checker/utils.h b/src/checker/utils.h index 8b580fe..22dc552 100644 --- a/src/checker/utils.h +++ b/src/checker/utils.h @@ -106,6 +106,7 @@ namespace ts::vm { *i += 2; break; } + case OP::Function: case OP::Union: case OP::Tuple: case OP::TemplateLiteral: diff --git a/src/checker/vm2.cpp b/src/checker/vm2.cpp index 55b1c33..25c4d72 100644 --- a/src/checker/vm2.cpp +++ b/src/checker/vm2.cpp @@ -48,6 +48,19 @@ namespace ts::vm2 { gcQueue[gcQueueIdx++] = type; } + inline Type * widen(Type * type) { + switch (type->kind) { + case TypeKind::Literal: { + if (type->flag & TypeFlag::StringLiteral) return allocate(TypeKind::String); + if (type->flag & TypeFlag::NumberLiteral) return allocate(TypeKind::Number); + if (type->flag & TypeFlag::BooleanLiteral) return allocate(TypeKind::Boolean); + if (type->flag & TypeFlag::BigIntLiteral) return allocate(TypeKind::BigInt); + } + } + + return type; + } + void gc(Type *type) { //debug("gc refCount={} {} ref={}", type->refCount, stringify(type), (void *) type); if (type->refCount>0) return; @@ -541,7 +554,7 @@ namespace ts::vm2 { auto frame = frames.at(i); debug("Frame {} depth={} variables={} initialSp={}", i, frame->depth, frame->variables, frame->initialSp); - if (top > 0) { + if (top>0) { auto size = top - frame->initialSp; for (unsigned int j = 0; jip, title, type->refCount, stringify(type), (void *) type); } @@ -602,22 +615,113 @@ namespace ts::vm2 { stack[sp++] = allocate(TypeKind::Null); break; } - //case OP::FrameEnd: { - // if (frame->size()>frame->variables) { - // //there is a return value on the stack, which we need to preserve - // auto ret = pop(); - // popFrame(); - // push(ret); - // } else { - // //throw away the whole stack - // popFrame(); - // } - // break; - //} - //case OP::Frame: { - // pushFrame(); - // break; - //} + case OP::Unknown: { + stack[sp++] = allocate(TypeKind::Unknown); + break; + } + case OP::Parameter: { + const auto address = activeSubroutine->parseUint32(); + auto type = allocate(TypeKind::Parameter); + type->readStorage(bin, address); + type->type = pop(); + stack[sp++] = type; + break; + } + case OP::Function: { + const auto size = activeSubroutine->parseUint16(); + auto types = frame->pop(size); + auto type = allocate(TypeKind::Function); + //first is always the return type + type->type = useAsRef(types[0]); + if (types.size()>1) { + auto current = (TypeRef *) type->type; + for_each(++types.begin(), types.end(), [¤t](auto v) { + current->next = useAsRef(v); + current = current->next; + }); + current->next = nullptr; + } + stack[sp++] = type; + break; + } + case OP::FunctionRef: { + const auto address = activeSubroutine->parseUint32(); + auto type = allocate(TypeKind::FunctionRef); + type->size = address; + stack[sp++] = type; + break; + } + case OP::Instantiate: { + const auto arguments = activeSubroutine->parseUint16(); + auto ref = pop(); //FunctionRef/Class + + switch (ref->kind) { + case TypeKind::FunctionRef: { + call(ref->size, arguments); + break; + } + default: { + throw std::runtime_error(fmt::format("Can not instantiate {}", ref->kind)); + } + } + break; + } + case OP::CallExpression: { + const auto parameterAmount = activeSubroutine->parseUint16(); + auto parameters = frame->pop(parameterAmount); + auto typeToCall = pop(); + + switch (typeToCall->kind) { + case TypeKind::Function: { + //it's important to handle parameters/typeArguments first before changing the stack with push() since `parameters` is a std::span. + auto current = (TypeRef *) typeToCall->type; + for (unsigned int i = 0;; i++) { + current = (TypeRef *) current->next; + if (!current) break; //end + auto parameter = current->type; + auto optional = isOptional(parameter); + if (i>parameters.size() - 1) { + //parameter not provided + if (!optional && !parameter->type) { + report(fmt::format("An argument for '{}' was not provided.", parameter->text), parameter); + } + break; + } + auto lvalue = parameters[i]; + //auto rvalue = reinterpret_pointer_cast(parameter); + //ExtendableStack stack; + if (!extends(lvalue, parameter)) { + //rerun again with + //report(stack.errorMessage()); + report(fmt::format("Argument of type '{}' is not assignable to parameter '{}' of type '{}'", stringify(lvalue), parameter->text, stringify(parameter))); + } + } + + //we could convert parameters to a tuple and then run isExtendable() on two tuples, + //necessary for REST parameters. + break; + } + default: { + throw std::runtime_error(fmt::format("CallExpression on {} not handled", typeToCall->kind)); + } + } + break; + } + case OP::Widen: { + auto type = pop(); + auto widened = widen(type); + push(widened); + if (widened != type) gc(type); + break; + } + case OP::Set: { + const auto address = activeSubroutine->parseUint32(); + auto type = pop(); + auto subroutine = activeSubroutine->module->getSubroutine(address); + if (subroutine->narrowed) drop(subroutine->narrowed); + subroutine->narrowed = use(type); + break; + } case OP::Assign: { auto rvalue = pop(); auto lvalue = pop(); diff --git a/src/tests/test_vm2.cpp b/src/tests/test_vm2.cpp index 77646f4..e4eb087 100644 --- a/src/tests/test_vm2.cpp +++ b/src/tests/test_vm2.cpp @@ -114,13 +114,13 @@ const v5: a = 'nope'; TEST_CASE("gc") { // The idea of garbage collection is this: Each type has refCount. // As soon as another type wants to hold on it, it increases refCount. - // This happens automatically once a user pop() from the stack. The stack itself - // is not an owner. If the user who pop()'d does not want to keep it, a drop(type) is necessary. + // This happens automatically once a user use(pop()) from the stack. The stack itself + // is not an owner. If the user who pop()'d does not want to keep it, a gc() call is necessary. // This schedules it for garbage collection. // If a type was received from somewhere else than the stack, the counter needs to be increased // manually by using use(type). // Subroutines like here `var1` keep the type for caching, hence this example has one active type - // and after clearing the module, all its subroutines reset their cache, so that is not no active type anymore. + // and after clearing the module, all its subroutines reset their cache, so that is not an active type anymore. string code = R"( type b = T; type a = b; @@ -134,7 +134,6 @@ const var1: a = false; ts::vm2::gcFlush(); //only var1 cached value should live - //todo: currently subroutines do not cache results because of TailCall. Is this good? REQUIRE(ts::vm2::pool.active == 1); ts::vm2::clear(module); @@ -395,11 +394,6 @@ const var1: F1<[]> = [0]; } TEST_CASE("vm2Fn5") { - //todo: make sure that temporary parameter are not GC inside the callee. - // currently `if (T->users == 1) {` is not true since T gets GC in ::Extends before [...T, 0] is reached - // the idea is to flag parameter [] as input, so that it doesn't get GC. We can't increase 'users' since that would mean - // we have no indicator whether it can be simply stolen in [...T]. - // question is when is <[]> GC? => always the stack that creates the objects should clear it. So when a Call is done, a Return cleans it up for it after switching to the caller. string code = R"( type F1 = T extends any ? [...T, 0] : never; const var1: F1<[]> = [0]; @@ -502,20 +496,6 @@ type F2 = T2 extends any ? T2 extends any ? F1 : 1 : 2; const var1: F2<[]> = [0]; )"; test(code, 0); - //todo: - // 1. Remove NJump, make Jump int32 - // 2. Find all tail calls. - // 2.1 Restructure the way subroutines are built? How? => Into Sections. An sections know where they are used (ConditionalTrue, ConditionalFalse, Distributive, MappedType, MappedTypeName) - // 2.2. Find all Call[] and check if it eventually hits a Return? (inefficient) => No - - //todo: Detect all tail calls in above - //what to consider - // conditional type - // distributive conditional type - // infer in conditional type - // mapped type - // tail call optimisation - // frames } TEST_CASE("vm2FnTailCallCondition") { @@ -524,10 +504,6 @@ type F1 = [...T1, 0]; type F2 = T2 extends any ? T2 extends any ? F1 : 1 : 2; const var1: F2<[]> = [0]; )"; - //todo: TailCall for all exits in JumpCondition and Distributive, even when nested `T extends x ? T extends y ? y : x : x` - // find all Call and look either - // - subsequent OP is Return, so convert it to TailCall - // - subsequent OP is Jump that leads eventually to Return, so convert it to TailCall test(code, 0); ts::vm2::gcFlush(); REQUIRE(ts::vm2::pool.active == 3); @@ -556,15 +532,44 @@ const var1: StringToNum<'999', []> = 1002; } testBench(code, 1); -// debug("active {}", ts::vm2::pool.active); -// ts::vm2::gcFlush(); -// debug("active gc {}", ts::vm2::pool.active); -// REQUIRE(ts::vm2::pool.active == 1); +} + +TEST_CASE("function1") { + string code = R"( + function doIt(v: T) { + } + const a = doIt; + a(23); +)"; + test(code, 0); + testBench(code, 0); +} + +TEST_CASE("function2") { + string code = R"( + function doIt(v: T) { + } + doIt(23); + doIt<34>(33); +)"; + test(code, 1); + testBench(code, 1); +} -// ts::bench("first", 10, [&] { -// module->clear(); -// run(module); -// }); +TEST_CASE("controlFlow1") { + string code = R"( + function boolFunc(t: true) {} + let bool = true; + boolFunc(bool); + + bool = false; + boolFunc(bool); + + bool = Date.now() > 1000 ? true : false; + boolFunc(bool); +)"; + test(code, 2); + testBench(code, 2); } TEST_CASE("vm2Cartesian") { @@ -646,8 +651,6 @@ TEST_CASE("bigUnion") { ts::bench("first", 1000, [&] { module->clear(); -// ts::vm2::clear(module); run(module); -// REQUIRE(process(ops) == 300 * 6); }); }