Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support for python syntax #8

Merged
merged 1 commit into from
Dec 11, 2024
Merged
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
9 changes: 3 additions & 6 deletions Main.lean
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
import NKL

def main (args : List String) : IO Unit :=
match args with
| .nil => IO.println s!"Hello, NKL!"
| .cons x _ => do
let s <- IO.FS.readFile x
NKL.parse_json s
def main : List String -> IO Unit
| [ file ] => IO.FS.readFile file >>= NKL.parse_json
| _ => IO.println "invalid arguments"
1 change: 1 addition & 0 deletions NKL.lean
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ Authors: Paul Govereau
import NKL.Encode
import NKL.FFI
import NKL.NKI
import NKL.Python
24 changes: 16 additions & 8 deletions NKL/FFI.lean
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,24 @@ Authors: Paul Govereau
import Lean
import NKL.NKI
import NKL.PrettyPrint
import NKL.Python

namespace NKL

-- temporary for testing
local instance : MonadLift (Except String) IO where
govereau marked this conversation as resolved.
Show resolved Hide resolved
monadLift
| .ok x => return x
| .error s => throw $ .userError s

@[export parse_json_old]
def parse_json_old (json : String) : IO Unit := do
let jsn <- Lean.Json.parse json
let f:Fun <- Lean.fromJson? jsn
print_nki f

@[export parse_json]
def parse_json (json : String) : IO Unit := do
match Lean.Json.parse json with
| .error str => throw $ .userError str
| .ok jsn => do
match Lean.fromJson? jsn with
| .error str => throw $ .userError str
| .ok (f:Fun) => print_nki f
def parse_json (s : String) : IO Unit := do
let kernel <- Python.Parsing.parse s
for (n,f) in kernel.funcs do
IO.println s!"found {n}"
IO.println s!"{repr f}"
288 changes: 288 additions & 0 deletions NKL/Python.lean
Original file line number Diff line number Diff line change
@@ -0,0 +1,288 @@
/-
Copyright (c) 2024 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Paul Govereau
-/
import Lean

/-!
# Abstract syntax of Python functions

Mostly 1-to-1 translation of the Python AST to lean.
see: https://docs.python.org/3/library/ast.html
-/

namespace NKL
namespace Python

deriving instance Repr for Lean.JsonNumber
govereau marked this conversation as resolved.
Show resolved Hide resolved

structure Pos where
lineno : Nat
end_lineno : Nat := 0
col_offset : Nat := 0
end_col_offset : Nat := 0
deriving Repr

inductive Const where
| none
| bool (value: Bool)
| num (value: Lean.JsonNumber)
| string (value: String)
| ellipsis
deriving Repr

inductive Ctx where
| load | store | del
govereau marked this conversation as resolved.
Show resolved Hide resolved
deriving Repr

mutual
inductive Expr where
| exprPos (expr : Expr') (pos : Pos)
deriving Repr

inductive Expr' where
| const (value: Const)
| name (id: String) (ctx : Ctx)
| attr (value : Expr) (id : String) (ctx : Ctx)
| tuple (xs: List Expr) (ctx : Ctx)
| list (xs: List Expr) (ctx : Ctx)
| subscript (tensor: Expr) (ix: List Expr) (ctx : Ctx)
| slice (l u step: Option Expr)
| boolOp (op : String) (values : List Expr)
| binOp (op : String) (left right : Expr)
| unaryOp (op : String) (operand : Expr)
| compare (left : Expr) (ops : List String) (comparators : List Expr)
| ifExp (test body orelse : Expr)
| call (f: Expr) (args: List Expr) (keywords : List Keyword)
deriving Repr

inductive Keyword where
| keyword (id : String) (value : Expr) (pos : Pos)
deriving Repr
end

mutual
inductive Stmt where
| stmtPos (stmt : Stmt') (pos : Pos)
deriving Repr

inductive Stmt' where
| pass
| expr (e : Expr)
| assert (e : Expr)
| ret (e: Expr)
| assign (xs: List Expr) (e: Expr)
| augAssign (x : Expr) (op : String) (e : Expr)
| annAssign (x : Expr) (annotation : Expr) (value : Option Expr)
| forLoop (x : Expr) (iter: Expr) (body: List Stmt) (orelse : List Stmt)
| ifStm (e : Expr) (thn els: List Stmt)
deriving Repr
end

structure Args where
posonlyargs : List String
args : List String
defaults: List Expr
vararg : Option String
kwonlyargs : List String
kw_defaults: List Expr
kwarg : Option String
deriving Repr

structure Fun where
source : String
args : Args
defaults: List Const
govereau marked this conversation as resolved.
Show resolved Hide resolved
body: List Stmt
deriving Repr

structure Kernel where
entry : String
funcs : List (String × Fun)
globals : List (String × Option String)

-------------------------------------------------------------------------------
-- Converting Python AST from Json

namespace Parsing
open Lean

-- I am using a state monad only to provide better error messages: the source
-- span (Pos) is saved while traversing the tree to identify the location
-- of any errors in the original program

abbrev Parser := EStateM String Pos

local instance : MonadLift (Except String) Parser where
monadLift
| .ok x => return x
| .error s => throw s

private def str : Json -> Parser String :=
monadLift ∘ Json.getStr?

private def field (f: Json -> Parser a) (j : Json) (name : String) : Parser a :=
j.getObjVal? name >>= f

private def field? (f: Json -> Parser a) (j : Json) (name : String) : Parser (Option a) :=
try let x <- field f j name; return (some x)
govereau marked this conversation as resolved.
Show resolved Hide resolved
catch _ => return none

private def list (f: Json -> Parser a) : Json -> Parser (List a)
| .arr arr => arr.toList.mapM f
| json => return [(<- f json )]

private def dict (f : Json -> Parser a) : Json -> Parser (List (String × a))
| .obj kvs => kvs.toArray.toList.mapM fun p => return (p.1, (<- f p.2))
| _ => throw s!"expecting dictionary"

private def opt (p : Json -> Parser a) : Json -> Parser (Option a)
| .null => return none
| j => return (some (<- p j))

-- Note: this will not fail, but can produce an invalid Pos
private def pos (j: Json) : Parser Pos :=
return {
lineno := (<- nat "lineno")
end_lineno := (<- nat "end_lineno")
col_offset := (<- nat "col_offset")
end_col_offset := (<- nat "end_col_offset")
}
where
nat (name : String) : Parser Nat :=
tryCatch (nat' name) fun _ => return 0
nat' (name : String) : Parser Nat := do
let obj <- j.getObjVal? name
Json.getNat? obj

private def withPos (p : String -> Json -> Parser b) (f : b -> Pos -> a) : Json -> Parser a
| .obj (.node _ _ key val _) => do
let pos <- pos val
set pos
let exp <- p key val
return (f exp pos)
| _ => throw "expecting object"

private def withSrc (source : String) (p : Parser a) : Parser a :=
try set { lineno := 0 : Pos } ; p
catch e => get >>= throw ∘ genError e
where
genError (err : String) (pos : Pos) : String :=
let lines := source.splitOn "\n"
let lineno := pos.lineno - 1
let colno := pos.col_offset
let line := if lines.length < lineno
then "<source not available>"
else lines[lineno]!
let indent := (Nat.repeat (List.cons ' ') colno List.nil).asString
s!"line {lineno}:\n{line}\n{indent}^-- {err}"

-------------------------------------------------------------------------------
-- Python AST Json objects

def const : Json -> Parser Const
| .null => return .none
| .bool b => return (.bool b)
| .num jn => return (.num jn)
| .str "..." => return .ellipsis
| .str s => return (.string s)
| _ => throw "expecting constant"

def exprCtx : Json -> Parser Ctx
| .str "Load" => return .load
| .str "Store" => return .store
| .str "Del" => return .del
| _ => throw "expecting ctx"

partial def expr (j : Json) : Parser Expr :=
withPos expr' Expr.exprPos j
where
expr' (key : String) (j : Json) : Parser Expr' := do
let strs := field (list str) j
let str := field str j
let ctx := field exprCtx j
let const := field const j
let exprs := field (list expr) j
let expr? := field (opt expr) j
let expr := field expr j
let keywords := field (list keyword) j
match key with
| "Constant" => return (.const (<- const "value"))
| "Name" => return (.name (<- str "id") (<- ctx "ctx"))
| "Attribute" => return (.attr (<- expr "value") (<- str "attr") (<- ctx "ctx"))
| "Tuple" => return (.tuple (<- exprs "elts") (<- ctx "ctx"))
| "List" => return (.list (<- exprs "elts") (<- ctx "ctx"))
| "Subscript" => return (.subscript (<- expr "value") (<- exprs "slice") (<- ctx "ctx"))
| "Slice" => return (.slice (<- expr? "lower") (<- expr? "upper") (<- expr? "step"))
| "BoolOp" => return (.boolOp (<- str "op") (<- exprs "values"))
| "BinOp" => return (.binOp (<- str "op") (<- expr "left") (<- expr "right"))
| "UnaryOp" => return (.unaryOp (<- str "op") (<- expr "operand"))
| "Compare" => return (.compare (<- expr "left") (<- strs "ops") (<- exprs "comparators"))
| "IfExp" => return (.ifExp (<- expr "test") (<- expr "body") (<- expr "orelse"))
| "Call" => return (.call (<- expr "func") (<- exprs "args") (<- keywords "keywords"))
| _ => throw s!"unsupported python construct {key}"

keyword (j: Json) : Parser Keyword := do
let j <- j.getObjVal? "keyword"
return ⟨ <- field str j "arg", <- field expr j "value", <- pos j ⟩

partial def stmt (j : Json) : Parser Stmt :=
withPos stmt' Stmt.stmtPos j
where
stmt' (key : String) (j : Json) : Parser Stmt' := do
govereau marked this conversation as resolved.
Show resolved Hide resolved
let str := field str j
let exprs := field (list expr) j
let expr? := field (opt expr) j
let expr := field expr j
let stmts := field (list stmt) j
match key with
| "Pass" => return .pass
| "Expr" => return (.expr (<- expr "value"))
| "Assert" => return (.assert (<- expr "test"))
| "Return" => return (.ret (<- expr "value"))
| "Assign" => return (.assign (<- exprs "targets") (<- expr "value"))
| "AugAssign" => return (.augAssign (<- expr "target") (<- str "op") (<- expr "value"))
| "AnnAssign" => return (.annAssign (<- expr "target") (<- expr "annotation") (<- expr? "value"))
| "For" => return (.forLoop (<- expr "target") (<- expr "iter") (<- stmts "body") (<- stmts "orelse"))
| "If" => return (.ifStm (<- expr "test") (<- stmts "body") (<- stmts "orelse"))
| _ => throw s!"unsupported python construct {key}"

def arguments (j : Json) : Parser Args := do
let obj <- j.getObjVal? "arguments"
let arg? := field (opt arg) obj
let args := field (list arg) obj
let exprs := field (list expr) obj
return {
posonlyargs := (<- args "posonlyargs")
args := (<- args "args")
defaults := (<- exprs "defaults")
vararg := (<- arg? "vararg")
kwonlyargs := (<- args "kwonlyargs")
kw_defaults := (<- exprs "kw_defaults")
kwarg := (<- arg? "kwarg")
}
where
arg (j : Json) : Parser String := do
let obj <- j.getObjVal? "arg"
return (<- field str obj "arg")

def function (j : Json) : Parser Fun := do
let source <- field str j "source"
withSrc source do
let args <- field arguments j "args"
let defaults <- field (list const) j "defaults"
let body <- field (list stmt) j "body"
return Fun.mk source args defaults body

def kernel (j : Json) : Parser Kernel := do
let name <- field str j "entry"
let funcs <- field (dict function) j "funcs"
let globals <- field (dict (opt str)) j "globals"
return Kernel.mk name funcs globals

def parse (s : String) : Except String Kernel := do
let jsn <- Json.parse s
match kernel jsn { lineno := 0 } with
| .ok x _ => .ok x
| .error s _ => .error s
Loading
Loading