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)
|