Skip to content

Commit f6ed488

Browse files
pirapiraclaude
andcommitted
Complete Phase 7d: stdlib system/OS (sys, os, time, datetime, pathlib, logging, signal, threading, tempfile)
Add 6 new stdlib modules and 3 stub modules: - sys: path, modules, argv, stdout/stderr (TextIOWrapper), exit, version, platform - os + os.path: getcwd, getenv, listdir, join, exists, isfile, isdir, dirname, basename, abspath, splitext - time: time, monotonic, sleep - datetime: datetime/timedelta/timezone classes with method dispatch and properties - pathlib: Path with properties (name/parent/stem/suffix), methods (exists/is_file/is_dir/resolve), / operator - logging: Logger with level-based output, getLogger, basicConfig - signal (stub): SIGINT/SIGTERM constants, no-op handler - threading (stub): Lock/RLock/Event as no-op context managers - tempfile (stub): mkdtemp, NamedTemporaryFile Also adds callBoundMethod fallback for print/str/repr/binop on builtin instance types, and SystemExit exception handling (excluded from bare except Exception). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fe2c398 commit f6ed488

13 files changed

Lines changed: 1425 additions & 15 deletions

File tree

CLAUDE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@ LeanPython/
5656
Hashlib.lean — hashlib: SHA-256, SHAKE-128 (pure Lean4), hash object dispatch
5757
Hmac.lean — hmac: HMAC-SHA256, HMAC object dispatch
5858
Secrets.lean — secrets: token_bytes, randbelow (using IO.rand)
59+
Sys.lean — sys: exit, TextIOWrapper for stdout/stderr
60+
Os.lean — os: getcwd, getenv, listdir; os.path: join, exists, dirname, etc.
61+
Time.lean — time: time, monotonic, sleep
62+
Datetime.lean — datetime: datetime, timedelta, timezone classes
63+
Pathlib.lean — pathlib: Path class with name/parent/stem/suffix/exists/etc.
64+
Logging.lean — logging: Logger, getLogger, basicConfig, level constants
5965
Main.lean — CLI entry point (reads .py file, parses, interprets)
6066
LeanPythonTest.lean — test driver root
6167
LeanPythonTest/

LeanPython/Interpreter/Eval.lean

Lines changed: 520 additions & 10 deletions
Large diffs are not rendered by default.

LeanPython/Runtime/Builtins.lean

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ import LeanPython.Stdlib.Json
88
import LeanPython.Stdlib.Hashlib
99
import LeanPython.Stdlib.Hmac
1010
import LeanPython.Stdlib.Secrets
11+
import LeanPython.Stdlib.Sys
12+
import LeanPython.Stdlib.Os
13+
import LeanPython.Stdlib.Time
14+
import LeanPython.Stdlib.Datetime
15+
import LeanPython.Stdlib.Pathlib
16+
import LeanPython.Stdlib.Logging
1117

1218
set_option autoImplicit false
1319

@@ -24,6 +30,10 @@ open LeanPython.Stdlib.Json
2430
open LeanPython.Stdlib.Hashlib
2531
open LeanPython.Stdlib.Hmac
2632
open LeanPython.Stdlib.Secrets
33+
open LeanPython.Stdlib.Sys
34+
open LeanPython.Stdlib.Os
35+
open LeanPython.Stdlib.Time
36+
open LeanPython.Stdlib.Logging
2737

2838
-- ============================================================
2939
-- Individual builtin implementations
@@ -834,6 +844,51 @@ partial def callBuiltin (name : String) (args : List Value)
834844
-- ============================================================
835845
| "json.dumps" => jsonDumps args kwargs
836846
| "json.loads" => jsonLoads args
847+
-- ============================================================
848+
-- sys module functions
849+
-- ============================================================
850+
| "sys.exit" => sysExit args
851+
-- ============================================================
852+
-- os module functions
853+
-- ============================================================
854+
| "os.getcwd" => osGetcwd args
855+
| "os.getenv" => osGetenv args
856+
| "os.listdir" => osListdir args
857+
-- ============================================================
858+
-- os.path module functions
859+
-- ============================================================
860+
| "os.path.join" => osPathJoin args
861+
| "os.path.exists" => osPathExists args
862+
| "os.path.isfile" => osPathIsfile args
863+
| "os.path.isdir" => osPathIsdir args
864+
| "os.path.dirname" => osPathDirname args
865+
| "os.path.basename" => osPathBasename args
866+
| "os.path.abspath" => osPathAbspath args
867+
| "os.path.splitext" => osPathSplitExt args
868+
| "os.path.normpath" => osPathNormpath args
869+
-- ============================================================
870+
-- time module functions
871+
-- ============================================================
872+
| "time.time" => timeTime args
873+
| "time.monotonic" => timeMonotonic args
874+
| "time.sleep" => timeSleep args
875+
-- ============================================================
876+
-- logging module functions
877+
-- ============================================================
878+
| "logging.basicConfig" => loggingBasicConfig args kwargs
879+
-- ============================================================
880+
-- signal module stubs
881+
-- ============================================================
882+
| "signal.signal" =>
883+
match args with
884+
| [_, handler] => return handler
885+
| _ => return .none
886+
-- ============================================================
887+
-- tempfile module stubs
888+
-- ============================================================
889+
| "tempfile.mkdtemp" => do
890+
let suffix ← (IO.rand 100000 999999 : IO Nat)
891+
return .str s!"/tmp/leanpy_{suffix}"
837892
| _ => throwNotImplemented s!"builtin '{name}' is not implemented"
838893

839894
end LeanPython.Runtime.Builtins

LeanPython/Runtime/Types.lean

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -316,9 +316,8 @@ def exceptionMatches (errorTypeName handlerTypeName : String) : Bool :=
316316
if handlerTypeName == "BaseException" then true
317317
else if handlerTypeName == "Exception" then
318318
-- Exception catches everything except BaseException-only subtypes
319-
-- (KeyboardInterrupt, SystemExit, GeneratorExit)
320-
-- For now, all our errors are Exception subclasses
321-
true
319+
errorTypeName != "SystemExit" && errorTypeName != "KeyboardInterrupt" &&
320+
errorTypeName != "GeneratorExit"
322321
else
323322
errorTypeName == handlerTypeName
324323

@@ -344,7 +343,8 @@ def builtinNames : List String :=
344343
"AttributeError", "OverflowError", "StopIteration",
345344
"NotImplementedError", "Exception", "BaseException",
346345
"NameError", "OSError", "IOError", "FileNotFoundError",
347-
"ImportError", "ModuleNotFoundError"]
346+
"ImportError", "ModuleNotFoundError", "SystemExit",
347+
"KeyboardInterrupt", "GeneratorExit"]
348348

349349
/-- Check if a name is a built-in function. -/
350350
def isBuiltinName (name : String) : Bool :=

LeanPython/Stdlib.lean

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,9 @@ import LeanPython.Stdlib.Json
77
import LeanPython.Stdlib.Hashlib
88
import LeanPython.Stdlib.Hmac
99
import LeanPython.Stdlib.Secrets
10+
import LeanPython.Stdlib.Sys
11+
import LeanPython.Stdlib.Os
12+
import LeanPython.Stdlib.Time
13+
import LeanPython.Stdlib.Datetime
14+
import LeanPython.Stdlib.Pathlib
15+
import LeanPython.Stdlib.Logging

LeanPython/Stdlib/Datetime.lean

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import LeanPython.Interpreter.Types
2+
3+
set_option autoImplicit false
4+
5+
namespace LeanPython.Stdlib.Datetime
6+
7+
open LeanPython.Runtime
8+
open LeanPython.Interpreter
9+
10+
-- ============================================================
11+
-- timedelta method dispatch
12+
-- ============================================================
13+
14+
/-- Dispatch methods on datetime.timedelta instances. -/
15+
partial def callTimedeltaMethod (iref : HeapRef) (method : String)
16+
(args : List Value) : InterpM Value := do
17+
let id_ ← heapGetInstanceData iref
18+
let days := match id_.attrs["_days"]? with
19+
| some (.int n) => n
20+
| some (.float f) => f.toUInt64.toNat
21+
| _ => 0
22+
let seconds := match id_.attrs["_seconds"]? with
23+
| some (.int n) => n
24+
| some (.float f) => f.toUInt64.toNat
25+
| _ => 0
26+
let microseconds := match id_.attrs["_microseconds"]? with
27+
| some (.int n) => n
28+
| _ => 0
29+
match method with
30+
| "total_seconds" =>
31+
match args with
32+
| [] =>
33+
let total : Float := Float.ofInt days * 86400.0 +
34+
Float.ofInt seconds + Float.ofInt microseconds / 1000000.0
35+
return .float total
36+
| _ => throwTypeError "total_seconds() takes no arguments"
37+
| "__str__" | "__repr__" =>
38+
let totalSec := days * 86400 + seconds
39+
let h := totalSec / 3600
40+
let m := (totalSec % 3600) / 60
41+
let s := totalSec % 60
42+
if days == 0 then
43+
return .str s!"{h}:{String.ofList (padLeft2 m)}{m}:{String.ofList (padLeft2 s)}{s}"
44+
else
45+
return .str s!"datetime.timedelta(days={days}, seconds={seconds})"
46+
| _ => throwAttributeError s!"'timedelta' object has no attribute '{method}'"
47+
where
48+
padLeft2 (n : Int) : List Char :=
49+
if n < 10 && n >= 0 then ['0'] else []
50+
51+
-- ============================================================
52+
-- datetime method dispatch
53+
-- ============================================================
54+
55+
/-- Dispatch methods on datetime.datetime instances. -/
56+
partial def callDatetimeMethod (iref : HeapRef) (method : String)
57+
(_args : List Value) : InterpM Value := do
58+
let id_ ← heapGetInstanceData iref
59+
let year := match id_.attrs["_year"]? with | some (.int n) => n | _ => 1970
60+
let month := match id_.attrs["_month"]? with | some (.int n) => n | _ => 1
61+
let day := match id_.attrs["_day"]? with | some (.int n) => n | _ => 1
62+
let hour := match id_.attrs["_hour"]? with | some (.int n) => n | _ => 0
63+
let minute := match id_.attrs["_minute"]? with | some (.int n) => n | _ => 0
64+
let second := match id_.attrs["_second"]? with | some (.int n) => n | _ => 0
65+
match method with
66+
| "isoformat" =>
67+
let sep := "T"
68+
return .str s!"{pad4 year}-{pad2 month}-{pad2 day}{sep}{pad2 hour}:{pad2 minute}:{pad2 second}"
69+
| "__str__" =>
70+
return .str s!"{pad4 year}-{pad2 month}-{pad2 day} {pad2 hour}:{pad2 minute}:{pad2 second}"
71+
| "__repr__" =>
72+
return .str s!"datetime.datetime({year}, {month}, {day}, {hour}, {minute}, {second})"
73+
| "year" => return .int year
74+
| "month" => return .int month
75+
| "day" => return .int day
76+
| "hour" => return .int hour
77+
| "minute" => return .int minute
78+
| "second" => return .int second
79+
| "timestamp" =>
80+
-- Simplified: return seconds since epoch (very rough)
81+
let daysSinceEpoch := (year - 1970) * 365 + (month - 1) * 30 + (day - 1)
82+
let totalSec := daysSinceEpoch * 86400 + hour * 3600 + minute * 60 + second
83+
return .float (Float.ofInt totalSec)
84+
| "replace" => return .instance iref -- simplified: just return self
85+
| _ => throwAttributeError s!"'datetime' object has no attribute '{method}'"
86+
where
87+
pad2 (n : Int) : String :=
88+
if n >= 0 && n < 10 then s!"0{n}" else toString n
89+
pad4 (n : Int) : String :=
90+
if n >= 0 && n < 10 then s!"000{n}"
91+
else if n >= 10 && n < 100 then s!"00{n}"
92+
else if n >= 100 && n < 1000 then s!"0{n}"
93+
else toString n
94+
95+
-- ============================================================
96+
-- timezone method dispatch
97+
-- ============================================================
98+
99+
/-- Dispatch methods on datetime.timezone instances. -/
100+
partial def callTimezoneMethod (_iref : HeapRef) (method : String)
101+
(_args : List Value) : InterpM Value := do
102+
match method with
103+
| "__str__" | "__repr__" => return .str "UTC"
104+
| "tzname" => return .str "UTC"
105+
| "utcoffset" => return .none
106+
| _ => throwAttributeError s!"'timezone' object has no attribute '{method}'"
107+
108+
end LeanPython.Stdlib.Datetime

LeanPython/Stdlib/Logging.lean

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import LeanPython.Interpreter.Types
2+
3+
set_option autoImplicit false
4+
5+
namespace LeanPython.Stdlib.Logging
6+
7+
open LeanPython.Runtime
8+
open LeanPython.Interpreter
9+
10+
-- ============================================================
11+
-- Logging level constants
12+
-- ============================================================
13+
14+
private def levelDEBUG : Int := 10
15+
private def levelINFO : Int := 20
16+
private def levelWARNING : Int := 30
17+
18+
-- ============================================================
19+
-- logging.basicConfig — no-op
20+
-- ============================================================
21+
22+
/-- Python logging.basicConfig(**kwargs): configure logging (no-op). -/
23+
partial def loggingBasicConfig (_args : List Value)
24+
(_kwargs : List (String × Value)) : InterpM Value := do
25+
return .none
26+
27+
-- ============================================================
28+
-- Create Logger instance (defined before callLoggerMethod so it can be used)
29+
-- ============================================================
30+
31+
/-- Create a Logger instance. -/
32+
partial def mkLoggerInstance (name : String) (level : Int) : InterpM Value := do
33+
let mut attrs : Std.HashMap String Value := {}
34+
attrs := attrs.insert "_name" (.str name)
35+
attrs := attrs.insert "_level" (.int level)
36+
attrs := attrs.insert "name" (.str name)
37+
let cls ← allocClassObj {
38+
name := "Logger", bases := #[], mro := #[], ns := {}, slots := none }
39+
match cls with
40+
| .classObj cref => heapSetClassData cref {
41+
name := "Logger", bases := #[], mro := #[cls], ns := {}, slots := none }
42+
| _ => pure ()
43+
let instRef ← heapAlloc (.instanceObjData { cls := cls, attrs := attrs })
44+
return .instance instRef
45+
46+
-- ============================================================
47+
-- Logger method dispatch
48+
-- ============================================================
49+
50+
private def levelName (l : Int) : String :=
51+
if l >= 50 then "CRITICAL"
52+
else if l >= 40 then "ERROR"
53+
else if l >= 30 then "WARNING"
54+
else if l >= 20 then "INFO"
55+
else "DEBUG"
56+
57+
/-- Dispatch methods on logging.Logger instances. -/
58+
partial def callLoggerMethod (iref : HeapRef) (method : String)
59+
(args : List Value) : InterpM Value := do
60+
let id_ ← heapGetInstanceData iref
61+
let loggerName := match id_.attrs["_name"]? with
62+
| some (.str n) => n
63+
| _ => "root"
64+
let level := match id_.attrs["_level"]? with
65+
| some (.int n) => n
66+
| _ => levelWARNING
67+
match method with
68+
| "debug" => logAtLevel levelDEBUG level loggerName args
69+
| "info" => logAtLevel levelINFO level loggerName args
70+
| "warning" | "warn" => logAtLevel levelWARNING level loggerName args
71+
| "error" => logAtLevel 40 level loggerName args
72+
| "critical" => logAtLevel 50 level loggerName args
73+
| "setLevel" =>
74+
match args with
75+
| [.int newLevel] => do
76+
let newAttrs := id_.attrs.insert "_level" (.int newLevel)
77+
heapSetInstanceData iref { id_ with attrs := newAttrs }
78+
return .none
79+
| _ => throwTypeError "setLevel() requires an integer argument"
80+
| "getChild" =>
81+
match args with
82+
| [.str childName] => do
83+
let fullName := loggerName ++ "." ++ childName
84+
mkLoggerInstance fullName level
85+
| _ => throwTypeError "getChild() requires a string argument"
86+
| "addHandler" => return .none -- no-op
87+
| "isEnabledFor" =>
88+
match args with
89+
| [.int msgLevel] => return .bool (msgLevel >= level)
90+
| _ => return .bool false
91+
| "getEffectiveLevel" => return .int level
92+
| "__repr__" => return .str s!"<Logger {loggerName} ({level})>"
93+
| _ => throwAttributeError s!"'Logger' object has no attribute '{method}'"
94+
where
95+
logAtLevel (msgLevel curLevel : Int) (lname : String)
96+
(logArgs : List Value) : InterpM Value := do
97+
if msgLevel >= curLevel then
98+
let msg := match logArgs with
99+
| [.str s] => s
100+
| [v] => Value.toStr v
101+
| _ => ""
102+
emitOutput s!"{levelName msgLevel}:{lname}:{msg}\n"
103+
return .none
104+
105+
end LeanPython.Stdlib.Logging

0 commit comments

Comments
 (0)