Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
File "invalid_tests/typechecking/allow_tracked_call_method.sk", line 4, characters 7-7:
@allow_tracked_call requires the method to be untracked
2 | class C() {
3 | @allow_tracked_call
4 | fun m(): String {
| ^
5 | "Fail\n"

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// @allow_tracked_call requires the annotated method to be untracked
class C() {
@allow_tracked_call
fun m(): String {
"Fail\n"
}
}

untracked fun main(): void {
print_raw(C().m())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
File "invalid_tests/typechecking/allow_tracked_call_override.sk", line 11, characters 7-7:
Invalid override for the field 'm' on child 'Y' for parent 'X'
9 | }
10 |
11 | class Y() extends X {
| ^
12 | untracked fun m(): String {

File "invalid_tests/typechecking/allow_tracked_call_override.sk", line 12, characters 17-17:
The type: untracked (() -> String)
10 |
11 | class Y() extends X {
12 | untracked fun m(): String {
| ^
13 | "Fail\n"

File "invalid_tests/typechecking/allow_tracked_call_override.sk", line 6, characters 29-29:
Is not a subtype of: (() -> String)
4 | base class X {
5 | @allow_tracked_call
6 | overridable untracked fun m(): String {
| ^
7 | "X\n"

File "invalid_tests/typechecking/allow_tracked_call_override.sk", line 6, characters 29-29:
This type was found to be a fixed upper bound and could not be changed to a compatible type.
To make these types compatible, annotate as this type: untracked (() -> String)
4 | base class X {
5 | @allow_tracked_call
6 | overridable untracked fun m(): String {
| ^
7 | "X\n"

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// an @allow_tracked_call method has a tracked type, so overriding it with
// a plain untracked method is an invalid override: calls through the base
// type could otherwise dispatch to an override that never opted in
base class X {
@allow_tracked_call
overridable untracked fun m(): String {
"X\n"
}
}

class Y() extends X {
untracked fun m(): String {
"Fail\n"
}
}

fun callIt(x: X): String {
x.m()
}

untracked fun main(): void {
print_raw(callIt(Y()))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
File "invalid_tests/typechecking/allow_tracked_call_tracked_fun.sk", line 3, characters 5-5:
@allow_tracked_call requires the function to be untracked
1 | // @allow_tracked_call requires the annotated function to be untracked
2 | @allow_tracked_call
3 | fun f(): String {
| ^
4 | "Fail\n"

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// @allow_tracked_call requires the annotated function to be untracked
@allow_tracked_call
fun f(): String {
"Fail\n"
}

fun main(): void {
print_raw(f())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
File "invalid_tests/typechecking/tracked_lambda_calls_untracked.sk", line 7, characters 13-15:
Cannot call an untracked function from a tracked context
5 |
6 | fun main(): void {
7 | g = () -> f();
| ^^^
8 | print_raw(g())

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// it's an error for a tracked lambda body to call an untracked function
untracked fun f(): String {
"Fail\n"
}

fun main(): void {
g = () -> f();
print_raw(g())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
File "invalid_tests/typechecking/tracked_lambda_solve_order.sk", line 13, characters 13-24:
Cannot call an untracked function from a tracked context
11 |
12 | fun trackedFn(): String {
13 | g = () -> sideEffect();
| ^^^^^^^^^^^^
14 | _ = taker(untracked () -> g());

Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// a tracked lambda defined in a tracked function keeps its tracked body
// check even when it is first called (and thus solved) inside an untracked
// lambda
untracked fun sideEffect(): String {
"Fail\n"
}

fun taker(_f: untracked () -> String): Int {
0
}

fun trackedFn(): String {
g = () -> sideEffect();
_ = taker(untracked () -> g());
g()
}

fun main(): void {
print_raw(trackedFn())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
File "invalid_tests/typechecking/untracked_pipe.sk", line 7, characters 13-25:
Cannot call an untracked function from a tracked context
5 |
6 | fun main(): void {
7 | print_raw("Fail\n" |> f)
| ^^^^^^^^^^^^^
8 | }

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// it's an error to pipe into an untracked function from a tracked context
untracked fun f(x: String): String {
x
}

fun main(): void {
print_raw("Fail\n" |> f)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
File "invalid_tests/typechecking/untracked_pipe_lambda.sk", line 3, characters 13-42:
Cannot call an untracked function from a tracked context
1 | // it's an error to pipe into an untracked lambda from a tracked context
2 | fun main(): void {
3 | print_raw("Fail\n" |> (untracked x -> x))
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
4 | }

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// it's an error to pipe into an untracked lambda from a tracked context
fun main(): void {
print_raw("Fail\n" |> (untracked x -> x))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
File "invalid_tests/typechecking/untracked_pipe_tparam.sk", line 8, characters 3-8:
Cannot call an untracked function from a tracked context
6 |
7 | fun callIt<F: untracked (String) -> String>(f: F, x: String): String {
8 | x |> f
| ^^^^^^
9 | }

Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// it's an error to pipe into a callee whose type parameter is bounded by
// an untracked function type, same as calling it directly
untracked fun side(x: String): String {
x
}

fun callIt<F: untracked (String) -> String>(f: F, x: String): String {
x |> f
}

fun main(): void {
print_raw(callIt(side, "Fail\n"))
}
8 changes: 8 additions & 0 deletions skiplang/compiler/src/OuterIstToIR.sk
Original file line number Diff line number Diff line change
Expand Up @@ -2385,6 +2385,10 @@ class Converter private (
paramTypes => funParamTypes,
returnType => in_.return_,
isMutable => false,
// Deliberately ignores @allow_tracked_call: the annotation only makes
// the declaration's type appear tracked to the type checker; the body
// is still side-effecting, so the optimizer must keep treating it as
// untracked (e.g. no CSE), like @debug.
isTracked => in_.untracked_.isNone(),
};

Expand Down Expand Up @@ -2491,6 +2495,10 @@ class Converter private (
useOptionalMask,
returnType => this.convertType(context, in_.return_),
isMutable => false,
// Deliberately ignores @allow_tracked_call: the annotation only makes
// the declaration's type appear tracked to the type checker; the body
// is still side-effecting, so the optimizer must keep treating it as
// untracked (e.g. no CSE), like @debug.
isTracked => in_.untracked_.isNone(),
};

Expand Down
21 changes: 7 additions & 14 deletions skiplang/compiler/src/peephole.sk
Original file line number Diff line number Diff line change
Expand Up @@ -390,20 +390,13 @@ private fun canCSECall(
(call match {
| CallFunctionBase{name} -> funCanCSE(name, env, call.pos)
| mcall @ CallMethodBase _ ->
// TODO: It's not clear what the rules should be for method call CSE,
// because some overrides may say @debug and some may not.
//
// To be honest, we don't care much about this case, so in general we
// allow CSE, but we allow a way for the determined user to turn it off.
// To avoid scanning huge numbers of subclasses here looking for @debug
// in any of them, if *any* do not say @debug we allow CSE, so the
// common check is quick.
//
// The user may expect saying @debug on the base class method should
// "contaminate" the subclasses, but we don't have a handy way to
// do that at the moment. We should revisit this later.
!optinfo.allMethodImplementations(mcall, env, (_, methodCodeID, _, _) ->
!funCanCSE(methodCodeID, env, call.pos)
// A dynamic dispatch may invoke any of the possible implementations,
// so only allow CSE when every one of them can: merging two calls
// that might dispatch to a @debug or untracked implementation would
// silently delete that implementation's second execution. The
// iteration stops at the first implementation that cannot CSE.
optinfo.allMethodImplementations(mcall, env, (_, methodCodeID, _, _) ->
funCanCSE(methodCodeID, env, call.pos)
)
})
}
Expand Down
46 changes: 43 additions & 3 deletions skiplang/compiler/src/skipNaming.sk
Original file line number Diff line number Diff line change
Expand Up @@ -1640,7 +1640,11 @@ fun make_method_fields(
}
});
purity = make_met_tfun_purity(met.kind);
tracking = make_tfun_tracking(met.untracked_);
tracking = make_tfun_tracking(
met.untracked_,
met.annotations,
met.name.i0,
);
ml = N.Tfun_modifiers{purity, tracking};
fty = (met.name.i0, N.Tfun(A.Vnone(), ml, Array[], params, met.return_));
abstract = met.body match {
Expand Down Expand Up @@ -1673,10 +1677,28 @@ fun make_met_tfun_purity(kind: N.FieldKind): N.Purity_modifier {
}
}

fun make_tfun_tracking(untracked_: ?FileRange): N.Tracking_modifier {
// The external tracking of a function or method: untracked declarations
// have untracked types, except that @allow_tracked_call makes the
// declaration appear tracked to callers (its body is still checked in an
// untracked context). Because the annotation is part of the type, calls,
// indirect calls through values, and override compatibility all follow
// from the ordinary tracking rules. Note that the opt-in is transitive:
// plain lambdas created inside the untracked body have tracked types, so
// an annotated declaration can hand untracked computations to tracked
// callers.
fun make_tfun_tracking(
untracked_: ?FileRange,
annotations: SSet,
pos: FileRange,
): N.Tracking_modifier {
untracked_ match {
| None() -> N.Ftracked()
| Some _ -> N.Funtracked()
| Some _ ->
if (annotationsContain(annotations, "@allow_tracked_call", pos)) {
N.Ftracked()
} else {
N.Funtracked()
}
}
}

Expand Down Expand Up @@ -1753,6 +1775,15 @@ fun method_def(
met: A.Method_def,
): N.Method_def {
(_position, method_name) = met.name;
if (
met.untracked_.isNone() &&
annotationsContain(met.annotations, "@allow_tracked_call", met.name.i0)
) {
SkipError.error(
met.name.i0,
"@allow_tracked_call requires the method to be untracked",
)
};
tparam_context = context_name + "<method:tparam>" + method_name + ":";
!env.in_deferred = met.deferred_.isSome();
tunder_count = sk_create_counter();
Expand Down Expand Up @@ -2167,6 +2198,15 @@ fun fun_def(
if (fd.body.isNone() && fd.native_.isNone()) {
SkipError.error(fd.name.i0, "Expected a function body")
};
if (
fd.untracked_.isNone() &&
annotationsContain(fd.annotations, "@allow_tracked_call", fd.name.i0)
) {
SkipError.error(
fd.name.i0,
"@allow_tracked_call requires the function to be untracked",
)
};
check_default_last(params);
params1 = params.map(p -> parameter(context, acc, env2, p));
return_1 = type(context, classAcc, env2, return_);
Expand Down
32 changes: 31 additions & 1 deletion skiplang/compiler/src/skipTyping.sk
Original file line number Diff line number Diff line change
Expand Up @@ -2513,7 +2513,11 @@ fun expr_(
acc,
fd.tparams,
);
tracking = SkipNaming.make_tfun_tracking(fd.untracked_);
tracking = SkipNaming.make_tfun_tracking(
fd.untracked_,
fd.annotations,
fd.name.i0,
);
params_ty = TUtils.make_params_type(
subst,
acc.tparam_constraints,
Expand Down Expand Up @@ -2809,6 +2813,28 @@ fun expr_(
pos,
N.Tfun(Ast.Vplus(), mods, Array[], Positional(Array[ty1]), ret_ty),
);
// Tracking is read with the pre-join subst: the join below binds any
// still-free type variable to ty_expect, whose modifiers are synthetic.
callee_tracking = Types.unfold_type(acc2.subst, ty2) match {
| (_, N.Tfun(_, fmods, _, _, _)) -> fmods.tracking
| (_, N.Tlambda(_, _, fmods, _)) -> fmods.tracking
| (_, N.Tparam(_, _, param_id, _)) ->
acc2.tparam_constraints
.findUppers(param_id)
.foldl(
(t, ty) ->
(t, Types.unfold_type(acc2.subst, ty)) match {
| (Some _, _) -> t
| (_, (_, N.Tfun(_, fmods, _, _, _))) -> Some(fmods.tracking)
| _ -> t
},
(None() : ?N.Tracking_modifier),
) match {
| Some(t) -> t
| None() -> N.Ftracked()
}
| _ -> N.Ftracked()
};
(acc3, _) = TUtils.join(
context,
next_id,
Expand All @@ -2819,6 +2845,7 @@ fun expr_(
ty2,
ty_expect,
);
check_tracking(pos, env, callee_tracking);
(acc3, (ret_ty, (pos, TAst.Call(e21, Positional(Array[(0, e11)])))))
| N.Binop((_, op @ N.Barbar()), e1, e2)
| N.Binop((_, op @ N.Ampamp()), e1, e2) ->
Expand Down Expand Up @@ -2937,6 +2964,8 @@ fun expr_(
env.frozen_level
},
lam_tfun_modifiers => mods,
lam_tracked_context => env.tracked_context &&
mods.tracking is N.Ftracked(),
yields,
};
lambda_list = List.Cons((lambda_id, lambda), acc1.lambda_list);
Expand Down Expand Up @@ -5293,6 +5322,7 @@ fun lambda(
break_type => None(),
frozen_level => lam.lam_frozen_level,
locals => lam_locals,
tracked_context => lam.lam_tracked_context,
};
!env = setReturnType(context, next_id, env, acc, rty, lam.yields);
ordered_args = args.map(t ->
Expand Down
Loading