2024-10-31 07:11:41 +00:00
// Usage: `node tolk-tester.js tests_dir` OR `node tolk-tester.js test_file.tolk`
// from current dir, providing some env (see getenv() calls).
// This is a JS version of tolk-tester.py to test Tolk compiled to WASM.
// Don't forget to keep it identical to Python version!
const fs = require ( 'fs' ) ;
const os = require ( 'os' ) ;
const path = require ( 'path' ) ;
const child _process = require ( 'child_process' ) ;
function print ( ... args ) {
console . log ( ... args )
}
/** @return {string} */
function getenv ( name , def = null ) {
if ( name in process . env )
return process . env [ name ]
if ( def === null ) {
print ( ` Environment variable ${ name } is not set ` )
process . exit ( 1 )
}
return def
}
const TOLKFIFTLIB _MODULE = getenv ( 'TOLKFIFTLIB_MODULE' )
const TOLKFIFTLIB _WASM = getenv ( 'TOLKFIFTLIB_WASM' )
const FIFT _EXECUTABLE = getenv ( 'FIFT_EXECUTABLE' )
const FIFT _LIBS _FOLDER = getenv ( 'FIFTPATH' ) // this env is needed for fift to work properly
2024-10-31 07:16:19 +00:00
const STDLIB _FOLDER = _ _dirname + '/../crypto/smartcont/tolk-stdlib'
2024-10-31 07:11:41 +00:00
const TMP _DIR = os . tmpdir ( )
class CmdLineOptions {
constructor ( /**string[]*/ argv ) {
if ( argv . length !== 3 ) {
print ( "Usage: node tolk-tester.js tests_dir OR node tolk-tester.js test_file.tolk" )
process . exit ( 1 )
}
if ( ! fs . existsSync ( argv [ 2 ] ) ) {
print ( ` Input ' ${ argv [ 2 ] } ' doesn't exist ` )
process . exit ( 1 )
}
if ( fs . lstatSync ( argv [ 2 ] ) . isDirectory ( ) ) {
this . tests _dir = argv [ 2 ]
this . test _file = null
} else {
this . tests _dir = path . dirname ( argv [ 2 ] )
this . test _file = argv [ 2 ]
}
}
/** @return {string[]} */
find _tests ( ) {
if ( this . test _file ) // an option to run (debug) a single test
return [ this . test _file ]
let tests = fs . readdirSync ( this . tests _dir ) . filter ( f => f . endsWith ( '.tolk' ) || f . endsWith ( '.ton' ) )
tests . sort ( )
return tests . map ( f => path . join ( this . tests _dir , f ) )
}
}
class ParseInputError extends Error {
}
class TolkCompilationFailedError extends Error {
constructor ( /**string*/ message , /**string*/ stderr ) {
super ( message ) ;
this . stderr = stderr
}
}
class TolkCompilationSucceededError extends Error {
}
class FiftExecutionFailedError extends Error {
constructor ( /**string*/ message , /**string*/ stderr ) {
super ( message ) ;
this . stderr = stderr
}
}
class CompareOutputError extends Error {
constructor ( /**string*/ message , /**string*/ output ) {
super ( message ) ;
this . output = output
}
}
class CompareFifCodegenError extends Error {
}
class CompareCodeHashError extends Error {
}
/ *
* In positive tests , there are several testcases "input X should produce output Y" .
* /
class TolkTestCaseInputOutput {
static reJustNumber = /^[-+]?\d+$/
static reMathExpr = /^[0x123456789()+\-*/<>]*$/
constructor ( /**string*/ method _id _str , /**string*/ input _str , /**string*/ output _str ) {
let processed _inputs = [ ]
for ( let in _arg of input _str . split ( ' ' ) ) {
if ( in _arg . length === 0 )
continue
else if ( in _arg . startsWith ( "x{" ) || TolkTestCaseInputOutput . reJustNumber . test ( in _arg ) )
processed _inputs . push ( in _arg )
else if ( TolkTestCaseInputOutput . reMathExpr . test ( in _arg ) )
// replace "3<<254" with "3n<<254n" (big number) before eval (in Python we don't need this)
processed _inputs . push ( eval ( in _arg . replace ( '//' , '/' ) . replace ( /(\d)($|\D)/gmi , '$1n$2' ) ) . toString ( ) )
else if ( in _arg === "null" )
processed _inputs . push ( "null" )
else
throw new ParseInputError ( ` ' ${ in _arg } ' can't be evaluated ` )
}
this . method _id = + method _id _str
this . input = processed _inputs . join ( ' ' )
this . expected _output = output _str
}
check ( /**string[]*/ stdout _lines , /**number*/ line _idx ) {
if ( stdout _lines [ line _idx ] !== this . expected _output )
throw new CompareOutputError ( ` error on case # ${ line _idx + 1 } ( ${ this . method _id } | ${ this . input } ): expected ' ${ this . expected _output } ', found ' ${ stdout _lines [ line _idx ] } ' ` , stdout _lines . join ( "\n" ) )
}
}
/ *
* @ stderr checks , when compilation fails , that stderr ( compilation error ) is expected .
* If it ' s multiline , all lines must be present in specified order .
* /
class TolkTestCaseStderr {
constructor ( /**string[]*/ stderr _pattern , /**boolean*/ avoid ) {
this . stderr _pattern = stderr _pattern
this . avoid = avoid
}
check ( /**string*/ stderr ) {
const line _match = this . find _pattern _in _stderr ( stderr . split ( /\n/ ) )
if ( line _match === - 1 && ! this . avoid )
throw new CompareOutputError ( "pattern not found in stderr:\n" +
this . stderr _pattern . map ( x => " " + x ) . join ( "\n" ) , stderr )
else if ( line _match !== - 1 && this . avoid )
throw new CompareOutputError ( ` pattern found (line ${ line _match + 1 } ), but not expected to be: \n ` +
this . stderr _pattern . map ( x => " " + x ) . join ( "\n" ) , stderr )
}
find _pattern _in _stderr ( /**string[]*/ stderr ) {
for ( let line _start = 0 ; line _start < stderr . length ; ++ line _start )
if ( this . try _match _pattern ( 0 , stderr , line _start ) )
return line _start
return - 1
}
try _match _pattern ( /**number*/ pattern _offset , /**string[]*/ stderr , /**number*/ offset ) {
if ( pattern _offset >= this . stderr _pattern . length )
return true
if ( offset >= stderr . length )
return false
const line _pattern = this . stderr _pattern [ pattern _offset ]
const line _output = stderr [ offset ]
return line _output . includes ( line _pattern ) && this . try _match _pattern ( pattern _offset + 1 , stderr , offset + 1 )
}
}
/ *
* @ fif _codegen checks that contents of compiled . fif matches the expected pattern .
* @ fif _codegen _avoid checks that is does not match the pattern .
* See comments in run _tests . py .
* /
class TolkTestCaseFifCodegen {
constructor ( /**string[]*/ fif _pattern , /**boolean*/ avoid ) {
/** @type {string[]} */
this . fif _pattern = fif _pattern . map ( s => s . trim ( ) )
this . avoid = avoid
}
check ( /**string[]*/ fif _output ) {
const line _match = this . find _pattern _in _fif _output ( fif _output )
if ( line _match === - 1 && ! this . avoid )
throw new CompareFifCodegenError ( "pattern not found:\n" +
this . fif _pattern . map ( x => " " + x ) . join ( "\n" ) )
else if ( line _match !== - 1 && this . avoid )
throw new CompareFifCodegenError ( ` pattern found (line ${ line _match + 1 } ), but not expected to be: \n ` +
this . fif _pattern . map ( x => " " + x ) . join ( "\n" ) )
}
find _pattern _in _fif _output ( /**string[]*/ fif _output ) {
for ( let line _start = 0 ; line _start < fif _output . length ; ++ line _start )
if ( this . try _match _pattern ( 0 , fif _output , line _start ) )
return line _start
return - 1
}
try _match _pattern ( /**number*/ pattern _offset , /**string[]*/ fif _output , /**number*/ offset ) {
if ( pattern _offset >= this . fif _pattern . length )
return true
if ( offset >= fif _output . length )
return false
const line _pattern = this . fif _pattern [ pattern _offset ]
const line _output = fif _output [ offset ]
if ( line _pattern !== "..." ) {
if ( ! TolkTestCaseFifCodegen . does _line _match ( line _pattern , line _output ) )
return false
return this . try _match _pattern ( pattern _offset + 1 , fif _output , offset + 1 )
}
while ( offset < fif _output . length ) {
if ( this . try _match _pattern ( pattern _offset + 1 , fif _output , offset ) )
return true
offset = offset + 1
}
return false
}
static split _line _to _cmd _and _comment ( /**string*/ trimmed _line ) {
const pos = trimmed _line . indexOf ( "//" )
if ( pos === - 1 )
return [ trimmed _line , null ]
else
return [ trimmed _line . substring ( 0 , pos ) . trimEnd ( ) , trimmed _line . substring ( pos + 2 ) . trimStart ( ) ]
}
static does _line _match ( /**string*/ line _pattern , /**string*/ line _output ) {
const [ cmd _pattern , comment _pattern ] = TolkTestCaseFifCodegen . split _line _to _cmd _and _comment ( line _pattern )
const [ cmd _output , comment _output ] = TolkTestCaseFifCodegen . split _line _to _cmd _and _comment ( line _output . trim ( ) )
return cmd _pattern === cmd _output && ( comment _pattern === null || comment _pattern === comment _output )
}
}
/ *
* @ code _hash checks that hash of compiled output . fif matches the provided value .
* It ' s used to "record" code boc hash and to check that it remains the same on compiler modifications .
* Being much less flexible than @ fif _codegen , it nevertheless gives a guarantee of bytecode stability .
* /
class TolkTestCaseExpectedHash {
constructor ( /**string*/ expected _hash ) {
this . code _hash = expected _hash
}
check ( /**string*/ fif _code _hash ) {
if ( this . code _hash !== fif _code _hash )
throw new CompareCodeHashError ( ` expected ${ this . code _hash } , actual ${ fif _code _hash } ` )
}
}
class TolkTestFile {
constructor ( /**string*/ tolk _filename , /**string*/ artifacts _folder ) {
this . line _idx = 0
this . tolk _filename = tolk _filename
this . artifacts _folder = artifacts _folder
this . compilation _should _fail = false
/** @type {TolkTestCaseStderr[]} */
this . stderr _includes = [ ]
/** @type {TolkTestCaseInputOutput[]} */
this . input _output = [ ]
/** @type {TolkTestCaseFifCodegen[]} */
this . fif _codegen = [ ]
/** @type {TolkTestCaseExpectedHash | null} */
this . expected _hash = null
/** @type {string | null} */
this . experimental _options = null
}
parse _input _from _tolk _file ( ) {
const lines = fs . readFileSync ( this . tolk _filename , 'utf-8' ) . split ( /\r?\n/ )
this . line _idx = 0
while ( this . line _idx < lines . length ) {
const line = lines [ this . line _idx ]
if ( line . startsWith ( '@testcase' ) ) {
let s = line . split ( "|" ) . map ( p => p . trim ( ) )
if ( s . length !== 4 )
throw new ParseInputError ( ` incorrect format of @testcase: ${ line } ` )
this . input _output . push ( new TolkTestCaseInputOutput ( s [ 1 ] , s [ 2 ] , s [ 3 ] ) )
} else if ( line . startsWith ( '@compilation_should_fail' ) ) {
this . compilation _should _fail = true
} else if ( line . startsWith ( '@stderr' ) ) {
this . stderr _includes . push ( new TolkTestCaseStderr ( this . parse _string _value ( lines ) , false ) )
} else if ( line . startsWith ( "@fif_codegen_avoid" ) ) {
this . fif _codegen . push ( new TolkTestCaseFifCodegen ( this . parse _string _value ( lines ) , true ) )
} else if ( line . startsWith ( "@fif_codegen" ) ) {
this . fif _codegen . push ( new TolkTestCaseFifCodegen ( this . parse _string _value ( lines ) , false ) )
} else if ( line . startsWith ( "@code_hash" ) ) {
this . expected _hash = new TolkTestCaseExpectedHash ( this . parse _string _value ( lines , false ) [ 0 ] )
} else if ( line . startsWith ( "@experimental_options" ) ) {
this . experimental _options = line . substring ( 22 )
}
this . line _idx ++
}
if ( this . input _output . length === 0 && ! this . compilation _should _fail )
throw new ParseInputError ( "no @testcase present" )
if ( this . input _output . length !== 0 && this . compilation _should _fail )
throw new ParseInputError ( "@testcase present, but compilation_should_fail" )
}
/** @return {string[]} */
parse _string _value ( /**string[]*/ lines , allow _multiline = true ) {
// a tag must be followed by a space (single-line), e.g. '@stderr some text'
// or be a multi-line value, surrounded by """
const line = lines [ this . line _idx ]
const pos _sp = line . indexOf ( ' ' )
const is _multi _line = lines [ this . line _idx + 1 ] === '"""'
const is _single _line = pos _sp !== - 1
if ( ! is _single _line && ! is _multi _line )
throw new ParseInputError ( ` ${ line } value is empty (not followed by a string or a multiline """) ` )
if ( is _single _line && is _multi _line )
throw new ParseInputError ( ` ${ line . substring ( 0 , pos _sp ) } value is both single-line and followed by """ ` )
if ( is _multi _line && ! allow _multiline )
throw new ParseInputError ( ` ${ line } value should be single-line ` ) ;
if ( is _single _line )
return [ line . substring ( pos _sp + 1 ) . trim ( ) ]
this . line _idx += 2
let s _multiline = [ ]
while ( this . line _idx < lines . length && lines [ this . line _idx ] !== '"""' ) {
s _multiline . push ( lines [ this . line _idx ] )
this . line _idx = this . line _idx + 1
}
return s _multiline
}
get _compiled _fif _filename ( ) {
return this . artifacts _folder + "/compiled.fif"
}
get _runner _fif _filename ( ) {
return this . artifacts _folder + "/runner.fif"
}
async run _and _check ( ) {
const wasmModule = await compileWasm ( TOLKFIFTLIB _MODULE , TOLKFIFTLIB _WASM )
let res = compileFile ( wasmModule , this . tolk _filename , this . experimental _options )
let exit _code = res . status === 'ok' ? 0 : 1
let stderr = res . message
let stdout = ''
if ( exit _code === 0 && this . compilation _should _fail )
throw new TolkCompilationSucceededError ( "compilation succeeded, but it should have failed" )
[Tolk] AST-based semantic analysis, get rid of Expr
This is a huge refactoring focusing on untangling compiler internals
(previously forked from FunC).
The goal is to convert AST directly to Op (a kind of IR representation),
doing all code analysis at AST level.
Noteable changes:
- AST-based semantic kernel includes: registering global symbols,
scope handling and resolving local/global identifiers,
lvalue/rvalue calc and check, implicit return detection,
mutability analysis, pure/impure validity checks,
simple constant folding
- values of `const` variables are calculated NOT based on CodeBlob,
but via a newly-introduced AST-based constant evaluator
- AST vertices are now inherited from expression/statement/other;
expression vertices have common properties (TypeExpr, lvalue/rvalue)
- symbol table is rewritten completely, SymDef/SymVal no longer exist,
lexer now doesn't need to register identifiers
- AST vertices have references to symbols, filled at different
stages of pipeline
- the remaining "FunC legacy part" is almost unchanged besides Expr
which was fully dropped; AST is converted to Ops (IR) directly
2024-12-16 18:19:45 +00:00
for ( let should _include of this . stderr _includes ) // @stderr is used to check errors and warnings
should _include . check ( stderr )
if ( exit _code !== 0 && this . compilation _should _fail )
2024-10-31 07:11:41 +00:00
return
if ( exit _code !== 0 && ! this . compilation _should _fail )
throw new TolkCompilationFailedError ( ` tolk exit_code = ${ exit _code } ` , stderr )
fs . writeFileSync ( this . get _compiled _fif _filename ( ) , ` "Asm.fif" include \n ${ res . fiftCode } ` )
{
let runner = ` " ${ this . get _compiled _fif _filename ( ) } " include <s constant code \n `
for ( let t of this . input _output )
runner += ` ${ t . input } ${ t . method _id } code 1 runvmx abort"exitcode is not 0" .s cr { drop } depth 1- times \n `
if ( this . expected _hash !== null )
runner += ` " ${ this . get _compiled _fif _filename ( ) } " include hash .s \n `
fs . writeFileSync ( this . get _runner _fif _filename ( ) , runner )
}
res = child _process . spawnSync ( FIFT _EXECUTABLE , [ this . get _runner _fif _filename ( ) ] )
exit _code = res . status
stderr = ( res . stderr || res . error ) . toString ( )
stdout = ( res . stdout || '' ) . toString ( )
if ( exit _code )
throw new FiftExecutionFailedError ( ` fift exit_code = ${ exit _code } ` , stderr )
let stdout _lines = stdout . split ( "\n" ) . map ( x => x . trim ( ) ) . filter ( s => s . length > 0 )
let fif _code _hash = null
if ( this . expected _hash !== null ) { // then the last stdout line is a hash
fif _code _hash = stdout _lines [ stdout _lines . length - 1 ]
stdout _lines = stdout _lines . slice ( 0 , stdout _lines . length - 1 )
}
if ( stdout _lines . length !== this . input _output . length )
throw new CompareOutputError ( ` unexpected number of fift output: ${ stdout _lines . length } lines, but ${ this . input _output . length } testcases ` , stdout )
for ( let i = 0 ; i < stdout _lines . length ; ++ i )
this . input _output [ i ] . check ( stdout _lines , i )
if ( this . fif _codegen . length ) {
const fif _output = fs . readFileSync ( this . get _compiled _fif _filename ( ) , 'utf-8' ) . split ( /\r?\n/ )
for ( let fif _codegen of this . fif _codegen )
fif _codegen . check ( fif _output )
}
if ( this . expected _hash !== null )
this . expected _hash . check ( fif _code _hash )
}
}
async function run _all _tests ( /**string[]*/ tests ) {
for ( let ti = 0 ; ti < tests . length ; ++ ti ) {
let tolk _filename = tests [ ti ]
print ( ` Running test ${ ti + 1 } / ${ tests . length } : ${ tolk _filename } ` )
let artifacts _folder = path . join ( TMP _DIR , tolk _filename )
let testcase = new TolkTestFile ( tolk _filename , artifacts _folder )
try {
if ( ! fs . existsSync ( artifacts _folder ) )
fs . mkdirSync ( artifacts _folder , { recursive : true } )
testcase . parse _input _from _tolk _file ( )
await testcase . run _and _check ( )
fs . rmSync ( artifacts _folder , { recursive : true } )
if ( testcase . compilation _should _fail )
print ( " OK, compilation failed as it should" )
else
print ( ` OK, ${ testcase . input _output . length } cases ` )
} catch ( e ) {
if ( e instanceof ParseInputError ) {
print ( ` Error parsing input (cur line # ${ testcase . line _idx + 1 } ): ` , e . message )
process . exit ( 2 )
} else if ( e instanceof TolkCompilationFailedError ) {
print ( " Error compiling tolk:" , e . message )
print ( " stderr:" )
print ( e . stderr . trimEnd ( ) )
process . exit ( 2 )
} else if ( e instanceof FiftExecutionFailedError ) {
print ( " Error executing fift:" , e . message )
print ( " stderr:" )
print ( e . stderr . trimEnd ( ) )
print ( " compiled.fif at:" , testcase . get _compiled _fif _filename ( ) )
process . exit ( 2 )
} else if ( e instanceof CompareOutputError ) {
print ( " Mismatch in output:" , e . message )
print ( " Full output:" )
print ( e . output . trimEnd ( ) )
print ( " Was compiled to:" , testcase . get _compiled _fif _filename ( ) )
process . exit ( 2 )
} else if ( e instanceof CompareFifCodegenError ) {
print ( " Mismatch in fif codegen:" , e . message )
print ( " Was compiled to:" , testcase . get _compiled _fif _filename ( ) )
print ( fs . readFileSync ( testcase . get _compiled _fif _filename ( ) , 'utf-8' ) )
process . exit ( 2 )
} else if ( e instanceof CompareCodeHashError ) {
print ( " Mismatch in code hash:" , e . message )
print ( " Was compiled to:" , testcase . get _compiled _fif _filename ( ) )
process . exit ( 2 )
}
throw e
}
}
}
const tests = new CmdLineOptions ( process . argv ) . find _tests ( )
print ( ` Found ${ tests . length } tests ` )
run _all _tests ( tests ) . then (
( ) => print ( ` Done, ${ tests . length } tests ` ) ,
console . error
)
// below are WASM helpers, which don't exist in Python version
process . setMaxListeners ( 0 ) ;
function copyToCString ( mod , str ) {
const len = mod . lengthBytesUTF8 ( str ) + 1 ;
const ptr = mod . _malloc ( len ) ;
mod . stringToUTF8 ( str , ptr , len ) ;
return ptr ;
}
function copyToCStringPtr ( mod , str , ptr ) {
const allocated = copyToCString ( mod , str ) ;
mod . setValue ( ptr , allocated , '*' ) ;
return allocated ;
}
2024-10-31 07:16:19 +00:00
/** @return {string} */
2024-10-31 07:11:41 +00:00
function copyFromCString ( mod , ptr ) {
return mod . UTF8ToString ( ptr ) ;
}
/** @return {{status: string, message: string, fiftCode: string, codeBoc: string, codeHashHex: string}} */
function compileFile ( mod , filename , experimentalOptions ) {
2024-10-31 07:16:19 +00:00
// see tolk-wasm.cpp: typedef void (*WasmFsReadCallback)(int, char const*, char**, char**)
2024-10-31 07:11:41 +00:00
const callbackPtr = mod . addFunction ( ( kind , dataPtr , destContents , destError ) => {
if ( kind === 0 ) { // realpath
try {
2024-10-31 07:16:19 +00:00
let relative = copyFromCString ( mod , dataPtr )
if ( relative . startsWith ( '@stdlib/' ) ) {
// import "@stdlib/filename" or import "@stdlib/filename.tolk"
relative = STDLIB _FOLDER + '/' + relative . substring ( 7 )
if ( ! relative . endsWith ( '.tolk' ) ) {
relative += '.tolk'
}
}
copyToCStringPtr ( mod , fs . realpathSync ( relative ) , destContents ) ;
2024-10-31 07:11:41 +00:00
} catch ( err ) {
copyToCStringPtr ( mod , 'cannot find file' , destError ) ;
}
} else if ( kind === 1 ) { // read file
try {
2024-10-31 07:16:19 +00:00
const absolute = copyFromCString ( mod , dataPtr ) // already normalized (as returned above)
copyToCStringPtr ( mod , fs . readFileSync ( absolute ) . toString ( 'utf-8' ) , destContents ) ;
2024-10-31 07:11:41 +00:00
} catch ( err ) {
copyToCStringPtr ( mod , err . message || err . toString ( ) , destError ) ;
}
} else {
copyToCStringPtr ( mod , 'Unknown callback kind=' + kind , destError ) ;
}
} , 'viiii' ) ;
const config = {
optimizationLevel : 2 ,
withStackComments : true ,
experimentalOptions : experimentalOptions || undefined ,
entrypointFileName : filename
} ;
const configPtr = copyToCString ( mod , JSON . stringify ( config ) ) ;
const responsePtr = mod . _tolk _compile ( configPtr , callbackPtr ) ;
return JSON . parse ( copyFromCString ( mod , responsePtr ) ) ;
}
async function compileWasm ( tolkFiftLibJsFileName , tolkFiftLibWasmFileName ) {
const wasmModule = require ( tolkFiftLibJsFileName )
const wasmBinary = new Uint8Array ( fs . readFileSync ( tolkFiftLibWasmFileName ) )
return await wasmModule ( { wasmBinary } )
}