File size: 9,681 Bytes
2e4432d
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
"""Public compile/check/run/serialize contract. Never resolves filesystem imports."""
import copy,hashlib,json,re
from pathlib import Path
from .frontend import parse,canonical_ast,pretty
from .checker import check,validate_value
from .runtime import ICLError,RuntimeContext,canonical,digest,DEFAULT_LIMITS
from .interpreter import execute_tree
from .vm import compile_bytecode,execute_bytecode
SCHEMA_VERSION='0.2.1'
SCHEMA_PATH=Path(__file__).with_name('OCIR_SCHEMA.json')
if not SCHEMA_PATH.exists():SCHEMA_PATH=Path(__file__).parent.parent/'OCIR_SCHEMA.json'
SCHEMA_SHA256=hashlib.sha256(SCHEMA_PATH.read_bytes()).hexdigest()
FEATURES=['functions','modules','lexical_scope','checked_int64','finite_float64','bounded_loop','list','record','tuple','option','result','tensor','observer','recovery','policy_effect','checkpoint','record_annotations','type_aliases','contextual_empty_lists']
NUMERIC={'int64':'checked_signed','integer_division':'truncate_toward_zero','float64':'finite_only','atol':1e-6,'rtol':1e-5,'normalize_epsilon':1e-5}

def load_program(source,module_map=None):
    modules=module_map or {}
    if not isinstance(modules,dict) or len(modules)>64:raise ICLError('IMPORT','module map must have <=64 entries')
    visiting=set();done={};graph=[];functions=[]
    root=parse(source)
    def visit(p,src,isroot=False):
        nm=p['module']
        if nm in visiting:raise ICLError('IMPORT_CYCLE','cyclic module import')
        if nm in done:return
        visiting.add(nm)
        if len(visiting)>32:raise ICLError('IMPORT_LIMIT','import nesting limit')
        for name in p['imports']:
            if name not in modules:raise ICLError('IMPORT','missing explicit module '+name)
            sub=parse(modules[name])
            if sub['module']!=name:raise ICLError('IMPORT','module declaration mismatch')
            visit(sub,modules[name])
        own={f['name'] for f in p['functions']}
        def rename(v):
            if isinstance(v,dict):
                if v.get('kind')=='call':
                    target=v['name']
                    if target in own and not isroot:v['name']=nm+'.'+target
                    elif '.' in target and target.split('.')[0] not in p['imports']:raise ICLError('IMPORT','qualified call without import')
                for child in v.values():rename(child)
            elif isinstance(v,list):
                for child in v:rename(child)
        rename(p)
        for f in p['functions']:
            if not isroot:f['name']=nm+'.'+f['name']
            functions.append(f)
        graph.append({'module':nm,'imports':p['imports'],'source_sha256':hashlib.sha256(src.encode()).hexdigest()})
        visiting.remove(nm);done[nm]=True
    visit(root,source,True)
    return {'module':root['module'],'imports':root['imports'],'functions':functions},graph

def compile_source(source,module_map=None,limits=None,entry='main'):
    p,graph=load_program(source,module_map);c=check(p);ctx=RuntimeContext(limits)
    if entry not in {f['name'] for f in p['functions']}:raise ICLError('NAME','entrypoint missing')
    source_hash=hashlib.sha256(source.encode()).hexdigest()
    ocir={'schema_version':SCHEMA_VERSION,'schema_sha256':SCHEMA_SHA256,'feature_set':list(FEATURES),'program_id':digest({'ast':canonical_ast(p),'modules':graph}),'source_sha256':source_hash,'module_graph':graph,'typed_blocks':p['functions'],'ops':c['ops'],'operand_types':[o['operand_types'] for o in c['ops']],'shape_constraints':c['shape_constraints'],'observer_context':{'mode':'explicit_metadata','implicit_conversion':False},'provenance_refs':[{'source_sha256':g['source_sha256'],'module':g['module'],'status':'COMPILED_LOCAL_SOURCE'} for g in graph],'required_effects':c['effects'],'limits':ctx.limits,'numeric_policy':dict(NUMERIC),'entrypoint':entry}
    ocir['integrity_sha256']=digest(ocir);return ocir

def check_source(source,**kwargs):return compile_source(source,**kwargs)
def canonicalize_source(source):return canonical_ast(parse(source))
def pretty_source(source):return pretty(parse(source))

AST_KEYS={
'literal':{'kind','value'},'var':{'kind','name'},'binary':{'kind','op','left','right'},'unary':{'kind','op','expr'},'call':{'kind','name','args'},'list':{'kind','items'},'tuple':{'kind','items'},'record':{'kind','fields'},'index':{'kind','target','index'},'field':{'kind','target','name'},'let':{'kind','name','type','mutable','expr'},'assign':{'kind','name','expr'},'return':{'kind','expr'},'expr':{'kind','expr'},'if':{'kind','cond','then','else'},'while':{'kind','cond','limit','body'}}
def validate_ocir(ocir):
    if not isinstance(ocir,dict):raise ICLError('OCIR_CORRUPT','object required')
    try:
        if ocir.get('schema_version')!=SCHEMA_VERSION or ocir.get('schema_sha256')!=SCHEMA_SHA256:raise ICLError('SCHEMA_INCOMPATIBLE','OCIR schema/hash differs')
        data={k:v for k,v in ocir.items() if k!='integrity_sha256'}
        if digest(data)!=ocir.get('integrity_sha256'):raise ICLError('CHECKSUM','OCIR integrity mismatch')
        required=set(json.loads(SCHEMA_PATH.read_text())['required'])
        if set(ocir)!=required:raise ICLError('OCIR_CORRUPT','unexpected/missing OCIR fields')
        if ocir['numeric_policy']!=NUMERIC or ocir['feature_set']!=FEATURES:raise ICLError('SCHEMA_INCOMPATIBLE','numeric/feature policy differs')
        ctx=RuntimeContext(ocir['limits']);ctx.check_value(ocir)
        count=0
        def walk(v,depth=0):
            nonlocal count
            count+=1
            if count>100000 or depth>128:raise ICLError('OCIR_CORRUPT','AST size/depth limit')
            if isinstance(v,dict):
                if 'kind' in v:
                    k=v['kind']
                    if k not in AST_KEYS:raise ICLError('UNSUPPORTED_OP','unknown AST opcode')
                    if set(v)-{'span','inferred_type'}!=AST_KEYS[k]:raise ICLError('OCIR_CORRUPT','malformed AST operation')
                    if k=='binary' and v['op'] not in ('+','-','*','/','%','==','!=','<','<=','>','>=','&&','||'):raise ICLError('UNSUPPORTED_OP','binary opcode')
                    if k=='unary' and v['op'] not in ('-','!'):raise ICLError('UNSUPPORTED_OP','unary opcode')
                    if k=='while' and (type(v['limit'])!=int or not 0<=v['limit']<=100000):raise ICLError('OCIR_CORRUPT','invalid loop limit')
                for child in v.values():walk(child,depth+1)
            elif isinstance(v,list):
                for child in v:walk(child,depth+1)
        walk(ocir['typed_blocks'])
        p={'module':ocir['module_graph'][-1]['module'],'imports':ocir['module_graph'][-1]['imports'],'functions':copy.deepcopy(ocir['typed_blocks'])};checked=check(p)
        if digest({'ast':canonical_ast(p),'modules':ocir['module_graph']})!=ocir['program_id']:raise ICLError('OCIR_CORRUPT','program identity inconsistent')
        if p['functions']!=ocir['typed_blocks'] or checked['ops']!=ocir['ops'] or checked['effects']!=ocir['required_effects'] or checked['shape_constraints']!=ocir['shape_constraints']:raise ICLError('OCIR_CORRUPT','typed data differs from checker')
        if ocir['operand_types']!=[o['operand_types'] for o in checked['ops']]:raise ICLError('OCIR_CORRUPT','operand types differ')
        if ocir['entrypoint'] not in {f['name'] for f in p['functions']}:raise ICLError('NAME','invalid entrypoint')
        return p
    except ICLError:raise
    except (KeyError,TypeError,ValueError,IndexError,RecursionError,OverflowError) as e:raise ICLError('OCIR_CORRUPT','invalid serialized structure: '+type(e).__name__)

def dumps_ocir(ocir):validate_ocir(ocir);return canonical(ocir)
def loads_ocir(text):
    try:
        if not isinstance(text,str) or len(text.encode())>4000000:raise ICLError('OCIR_CORRUPT','serialized size limit')
    except UnicodeError:raise ICLError('OCIR_CORRUPT','invalid Unicode')
    def pairs(items):
        d={}
        for k,v in items:
            if k in d:raise ICLError('OCIR_CORRUPT','duplicate key')
            d[k]=v
        return d
    try:o=json.loads(text,object_pairs_hook=pairs,parse_constant=lambda x:(_ for _ in ()).throw(ICLError('NONFINITE','JSON nonfinite')))
    except (ValueError,RecursionError):raise ICLError('OCIR_CORRUPT','invalid JSON')
    validate_ocir(o);return o

def run_ocir(ocir,args=None,engine='vm',entry=None,policy=None,limits=None,checkpoint=None,stop_after=None):
    p=validate_ocir(ocir);entry=entry or ocir['entrypoint'];args=[] if args is None else args
    requested=RuntimeContext(limits).limits if limits is not None else ocir['limits']
    lim={k:min(requested[k],ocir['limits'][k]) for k in DEFAULT_LIMITS};ctx=RuntimeContext(lim,policy)
    fn=next((f for f in p['functions'] if f['name']==entry),None)
    if fn is None:raise ICLError('NAME','entry missing')
    if not isinstance(args,list) or len(args)!=len(fn['params']):raise ICLError('TYPE','entry argument count')
    for (_,tp),v in zip(fn['params'],args):validate_value(v,tp,ctx)
    if engine=='tree':
        if checkpoint is not None or stop_after is not None:raise ICLError('UNSUPPORTED_OP','tree checkpoint unsupported; VM checkpoint supported')
        return execute_tree(p,entry,args,lim,policy)
    if engine!='vm':raise ICLError('ENGINE','unknown execution engine')
    out=execute_bytecode(compile_bytecode(p),entry,args,lim,policy,checkpoint,stop_after)
    if 'value' in out and out.get('checkpoint') is None:validate_value(out['value'],fn['return_type'],ctx)
    return out

def run_source(source,args=None,engine='vm',entry='main',module_map=None,limits=None,policy=None,checkpoint=None,stop_after=None):
    return run_ocir(compile_source(source,module_map,limits,entry),args,engine,entry,policy,limits,checkpoint,stop_after)