Skip to content

Commit 5cd6ef4

Browse files
Add optional tz parameter to datetime_now and reject extra positional args (#725)
### Problem `datetime.now(timezone.utc)` caused an arity mismatch because the prelude's `datetime_now` declared 0 parameters while the Python call passed 1 argument. This surfaced as a "call [end_time] := datetime_now($hole_12($heap_in))" error in check_storage_costs. ### Approach Rather than silently dropping extra positional arguments (which could mask real bugs), we model the missing optional parameter in the prelude and add an arity check that rejects extra positional arguments for all functions. **Prelude changes:** - Add `tz: Any` parameter to `datetime_now` in the Laurel prelude - Add `tz: AnyOrNone` parameter to `datetime_now` in the Core prelude and FunctionSignatures **Arity checking:** `combinePositionalAndKeywordArgs` now raises a user error when a call passes more positional arguments than the function signature declares, for both user-defined and prelude functions alike By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. --------- Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
1 parent d6e9641 commit 5cd6ef4

8 files changed

Lines changed: 63 additions & 11 deletions

File tree

Strata/Languages/Python/CorePrelude.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ function Datetime_get_timedelta(d : Datetime) : int;
217217
// means subtracting an 'old' timestamp from a 'new' timestamp may return
218218
// a negative difference.
219219

220-
procedure datetime_now() returns (d:Datetime, maybe_except: ExceptOrNone)
220+
procedure datetime_now(tz: AnyOrNone) returns (d:Datetime, maybe_except: ExceptOrNone)
221221
spec {
222222
ensures (Datetime_get_timedelta(d) == Timedelta_mk(0,0,0));
223223
}

Strata/Languages/Python/FunctionSignatures.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ def addCoreDecls : SignatureM Unit := do
136136
decl "json_loads" [msg :< string]
137137
decl "input" [msg :< string]
138138
decl "random_choice" [l :< ListStr]
139-
decl "datetime_now" []
139+
decl "datetime_now" [tz :< AnyOrNone]
140140
decl "datetime_utcnow" []
141141
decl "datetime_date" [dt :< Datetime]
142142
decl "timedelta" [ days :< IntOrNone, hours :< IntOrNone]

Strata/Languages/Python/PythonRuntimeLaurelPart.lean

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -937,7 +937,7 @@ procedure datetime_date(d: Any) returns (ret: Any, error: Error)
937937
}
938938
};
939939

940-
procedure datetime_now() returns (ret: Any)
940+
procedure datetime_now(tz: Any) returns (ret: Any)
941941
ensures Any..isfrom_datetime(ret) summary "ret_type"
942942
{
943943
var d: int;

Strata/Languages/Python/PythonToLaurel.lean

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,10 @@ partial def combinePositionalAndKeywordArgs
839839
throwUserError callRange
840840
s!"'{name}' called with unknown keyword arguments: {extraNames}"
841841
let kwords := pyKwordsToHashMap kwords
842+
-- Extra positional args beyond the signature are an arity error.
843+
if posArgs.length > funcDecl.args.length then
844+
throwUserError callRange
845+
s!"'{name}' called with too many positional arguments: expected at most {funcDecl.args.length}, got {posArgs.length}"
842846
let unprovidedPosArgs := funcDecl.args.drop posArgs.length
843847
--every unprovided positional args must have a default value in the function signature or be provided in the kwargs
844848
let missingArgs := unprovidedPosArgs.filter fun arg =>
@@ -926,6 +930,10 @@ partial def translateCall (ctx : TranslationContext)
926930
-- expand the dictionary into individual arguments using DictStrAny_get
927931
if isVarKwargs kwords && funcDecl.isSome then
928932
let funcDecl := funcDecl.get!
933+
let name := if methodName.isEmpty then funcDecl.name else methodName
934+
if args.length > funcDecl.args.length then
935+
throwUserError callRange
936+
s!"'{name}' called with too many positional arguments: expected at most {funcDecl.args.length}, got {args.length}"
929937
let trans_posArgs ← args.mapM (translateExpr ctx)
930938
let trans_dict ← translateVarKwargs ctx kwords
931939
let remainingParams := funcDecl.args.drop args.length

StrataTest/Languages/Python/PreludeVerifyTest.lean

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -158,15 +158,15 @@ Obligation: postcondition
158158
Property: assert
159159
Result: ✅ pass
160160
161-
Obligation: assert(41763)
161+
Obligation: assert(41770)
162162
Property: assert
163163
Result: ✅ pass
164164
165-
Obligation: assert(41830)
165+
Obligation: assert(41837)
166166
Property: assert
167167
Result: ✅ pass
168168
169-
Obligation: assert(41938)
169+
Obligation: assert(41945)
170170
Property: assert
171171
Result: ✅ pass
172172

StrataTest/Languages/Python/VerifyPythonTest.lean

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ Python → Laurel → Core → SMT pipeline and produces diagnostics.
1717
namespace Strata.Python.VerifyPythonTest
1818

1919
open StrataTest.Util
20-
open Strata.Python (processPythonFile withPython)
20+
open Strata.Python (processPythonFile withPython containsSubstr)
2121
open Strata.Parser (stringInputContext)
2222

2323
-- Passing assertions produce no diagnostics.
@@ -130,16 +130,16 @@ open Strata.Parser (stringInputContext)
130130
if diags.size ≠ 0 then
131131
throw <| .userError s!"Expected 0 diagnostics, got {diags.size}"
132132

133-
-- Multi-output prelude procedures: timedelta_func returns (delta: Any, maybe_except: Error).
134-
-- This tests that withException detects the multi-output signature and generates
135-
-- a 2-target assignment, and that computeExprType returns Unknown (→ Any in Core).
133+
-- datetime.now() with optional tz parameter and timedelta arithmetic.
134+
-- Also exercises multi-output prelude procedure detection (timedelta_func
135+
-- returns (delta: Any, maybe_except: Error)).
136136
#guard_msgs in
137137
#eval withPython (warnOnSkip := false) fun pythonCmd => do
138138
let program :=
139139
"from datetime import datetime, timedelta
140140
141141
def main() -> None:
142-
now: datetime = datetime.now()
142+
now: datetime = datetime.now(None)
143143
delta: timedelta = timedelta(days=7)
144144
start: datetime = now - delta
145145
assert start <= now
@@ -148,6 +148,43 @@ def main() -> None:
148148
if diags.size ≠ 0 then
149149
throw <| .userError s!"Expected 0 diagnostics, got {diags.size}: {diags.map (·.message)}"
150150

151+
-- Calling a user-defined function with too many positional args should error.
152+
#guard_msgs in
153+
#eval withPython (warnOnSkip := false) fun pythonCmd => do
154+
let program :=
155+
"def greet(name: str) -> str:
156+
return name
157+
158+
def main() -> None:
159+
x: str = greet(\"alice\", \"extra\")
160+
"
161+
try
162+
let _ ← processPythonFile pythonCmd (stringInputContext "test.py" program)
163+
throw <| IO.userError "Expected pipeline error for too many positional arguments"
164+
catch e =>
165+
let msg := toString e
166+
unless containsSubstr msg "too many positional arguments" do
167+
throw <| IO.userError s!"Expected 'too many positional arguments' error, got: {msg}"
168+
169+
-- Extra positional args with **kwargs expansion should also error.
170+
#guard_msgs in
171+
#eval withPython (warnOnSkip := false) fun pythonCmd => do
172+
let program :=
173+
"def greet(name: str) -> str:
174+
return name
175+
176+
def main() -> None:
177+
d: dict = {}
178+
x: str = greet(\"alice\", \"extra\", **d)
179+
"
180+
try
181+
let _ ← processPythonFile pythonCmd (stringInputContext "test.py" program)
182+
throw <| IO.userError "Expected pipeline error for too many positional arguments"
183+
catch e =>
184+
let msg := toString e
185+
unless containsSubstr msg "too many positional arguments" do
186+
throw <| IO.userError s!"Expected 'too many positional arguments' error, got: {msg}"
187+
151188
-- Returning a Composite-typed value from a function with Any return type
152189
-- should not crash; the Composite is replaced with a Hole (unconstrained value).
153190
#guard_msgs in

StrataTest/Languages/Python/tests/cbmc_expected.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,6 @@ test_class_with_methods.py.ion SKIP
3131
test_module_level.py.ion SKIP
3232
test_if_elif.py.ion SKIP
3333
test_variable_reassign.py.ion SKIP
34+
test_datetime_now_tz.py.ion SKIP
3435
test_timedelta_expr.py.ion SKIP
3536
test_composite_return.py.ion PASS
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from datetime import datetime, timezone, timedelta
2+
3+
now: datetime = datetime.now(timezone.utc)
4+
delta: timedelta = timedelta(days=7)
5+
start: datetime = now - delta
6+
assert start <= now

0 commit comments

Comments
 (0)